mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: consolidate dialogs (#4506)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
7bcc51863f
commit
ea6d1ffa32
59 changed files with 2042 additions and 1659 deletions
|
|
@ -42,7 +42,6 @@ import androidx.compose.foundation.text.TextAutoSize
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
|
|
@ -136,6 +135,7 @@ import org.meshtastic.core.strings.i_know_what_i_m_doing
|
|||
import org.meshtastic.core.strings.learn_more
|
||||
import org.meshtastic.core.strings.okay
|
||||
import org.meshtastic.core.strings.save
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.icon.Bluetooth
|
||||
import org.meshtastic.core.ui.icon.CheckCircle
|
||||
import org.meshtastic.core.ui.icon.CloudDownload
|
||||
|
|
@ -208,24 +208,17 @@ fun FirmwareUpdateScreen(
|
|||
androidx.activity.compose.BackHandler(enabled = shouldKeepFirmwareScreenOn(state)) { showExitConfirmation = true }
|
||||
|
||||
if (showExitConfirmation) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showExitConfirmation = false },
|
||||
title = { Text(stringResource(Res.string.firmware_update_disclaimer_title)) },
|
||||
text = { Text(stringResource(Res.string.firmware_update_disconnect_warning)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showExitConfirmation = false
|
||||
viewModel.cancelUpdate()
|
||||
navController.navigateUp()
|
||||
},
|
||||
) {
|
||||
Text(stringResource(Res.string.firmware_update_retry)) // Use "Cancel & Exit" if available
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showExitConfirmation = false }) { Text(stringResource(Res.string.back)) }
|
||||
MeshtasticDialog(
|
||||
onDismiss = { showExitConfirmation = false },
|
||||
title = stringResource(Res.string.firmware_update_disclaimer_title),
|
||||
message = stringResource(Res.string.firmware_update_disconnect_warning),
|
||||
confirmText = stringResource(Res.string.firmware_update_retry),
|
||||
onConfirm = {
|
||||
showExitConfirmation = false
|
||||
viewModel.cancelUpdate()
|
||||
navController.navigateUp()
|
||||
},
|
||||
dismissText = stringResource(Res.string.back),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -387,7 +380,7 @@ private fun ReadyState(
|
|||
if (showDisclaimer) {
|
||||
DisclaimerDialog(
|
||||
updateMethod = state.updateMethod,
|
||||
onDismissRequest = { showDisclaimer = false },
|
||||
onDismiss = { showDisclaimer = false },
|
||||
onConfirm = {
|
||||
showDisclaimer = false
|
||||
if (selectedReleaseType == FirmwareReleaseType.LOCAL) {
|
||||
|
|
@ -450,10 +443,13 @@ private fun ReadyState(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun DisclaimerDialog(updateMethod: FirmwareUpdateMethod, onDismissRequest: () -> Unit, onConfirm: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(stringResource(Res.string.firmware_update_disclaimer_title)) },
|
||||
private fun DisclaimerDialog(updateMethod: FirmwareUpdateMethod, onDismiss: () -> Unit, onConfirm: () -> Unit) {
|
||||
MeshtasticDialog(
|
||||
onDismiss = onDismiss,
|
||||
title = stringResource(Res.string.firmware_update_disclaimer_title),
|
||||
confirmText = stringResource(Res.string.i_know_what_i_m_doing),
|
||||
onConfirm = onConfirm,
|
||||
dismissText = stringResource(Res.string.cancel),
|
||||
text = {
|
||||
Column(modifier = Modifier.animateContentSize(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(stringResource(Res.string.firmware_update_disclaimer_text))
|
||||
|
|
@ -478,8 +474,6 @@ private fun DisclaimerDialog(updateMethod: FirmwareUpdateMethod, onDismissReques
|
|||
}
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(onClick = onConfirm) { Text(stringResource(Res.string.i_know_what_i_m_doing)) } },
|
||||
dismissButton = { TextButton(onClick = onDismissRequest) { Text(stringResource(Res.string.cancel)) } },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -757,20 +751,16 @@ private fun AwaitingFileSaveState(state: FirmwareUpdateState.AwaitingFileSave, o
|
|||
var showDialog by remember { mutableStateOf(true) }
|
||||
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { /* No-op to force user to acknowledge */ },
|
||||
title = { Text(stringResource(Res.string.firmware_update_usb_instruction_title)) },
|
||||
text = { Text(stringResource(Res.string.firmware_update_usb_instruction_text)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showDialog = false
|
||||
onSaveFile(state.fileName)
|
||||
},
|
||||
) {
|
||||
Text(stringResource(Res.string.okay))
|
||||
}
|
||||
MeshtasticDialog(
|
||||
onDismiss = { /* No-op to force user to acknowledge */ },
|
||||
title = stringResource(Res.string.firmware_update_usb_instruction_title),
|
||||
confirmText = stringResource(Res.string.okay),
|
||||
onConfirm = {
|
||||
showDialog = false
|
||||
onSaveFile(state.fileName)
|
||||
},
|
||||
text = { Text(stringResource(Res.string.firmware_update_usb_instruction_text)) },
|
||||
dismissable = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.map.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -28,7 +27,6 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -37,7 +35,6 @@ import androidx.compose.material3.ListItem
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
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
|
||||
|
|
@ -67,6 +64,7 @@ import org.meshtastic.core.strings.url_cannot_be_empty
|
|||
import org.meshtastic.core.strings.url_must_contain_placeholders
|
||||
import org.meshtastic.core.strings.url_template
|
||||
import org.meshtastic.core.strings.url_template_hint
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.feature.map.MapViewModel
|
||||
|
||||
|
|
@ -191,16 +189,13 @@ private fun AddEditCustomTileProviderDialog(
|
|||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
if (config == null) {
|
||||
stringResource(Res.string.add_custom_tile_source)
|
||||
} else {
|
||||
stringResource(Res.string.edit_custom_tile_source)
|
||||
},
|
||||
)
|
||||
MeshtasticDialog(
|
||||
onDismiss = onDismiss,
|
||||
title =
|
||||
if (config == null) {
|
||||
stringResource(Res.string.add_custom_tile_source)
|
||||
} else {
|
||||
stringResource(Res.string.edit_custom_tile_source)
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
|
|
@ -235,8 +230,9 @@ private fun AddEditCustomTileProviderDialog(
|
|||
)
|
||||
}
|
||||
},
|
||||
confirmButton = { Button(onClick = { validateAndSave() }) { Text(stringResource(Res.string.save)) } },
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(Res.string.cancel)) } },
|
||||
onConfirm = { validateAndSave() },
|
||||
confirmTextRes = Res.string.save,
|
||||
dismissTextRes = Res.string.cancel,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ package org.meshtastic.feature.messaging
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -36,6 +34,7 @@ import org.meshtastic.core.strings.close
|
|||
import org.meshtastic.core.strings.message_retry_count
|
||||
import org.meshtastic.core.strings.relays
|
||||
import org.meshtastic.core.strings.resend
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
@Composable
|
||||
|
|
@ -49,28 +48,12 @@ fun DeliveryInfo(
|
|||
maxRetries: Int = 0,
|
||||
onConfirm: (() -> Unit) = {},
|
||||
onDismiss: () -> Unit = {},
|
||||
) = AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
dismissButton = {
|
||||
FilledTonalButton(onClick = onDismiss, modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||
Text(text = stringResource(Res.string.close))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
if (resendOption) {
|
||||
FilledTonalButton(onClick = onConfirm, modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||
Text(text = stringResource(Res.string.resend))
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(title),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
},
|
||||
) = MeshtasticDialog(
|
||||
title = stringResource(title),
|
||||
onDismiss = onDismiss,
|
||||
dismissText = stringResource(Res.string.close),
|
||||
confirmText = if (resendOption) stringResource(Res.string.resend) else null,
|
||||
onConfirm = if (resendOption) onConfirm else null,
|
||||
text = {
|
||||
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
text?.let {
|
||||
|
|
@ -98,6 +81,4 @@ fun DeliveryInfo(
|
|||
}
|
||||
}
|
||||
},
|
||||
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ import androidx.compose.material.icons.rounded.SelectAll
|
|||
import androidx.compose.material.icons.rounded.SpeakerNotesOff
|
||||
import androidx.compose.material.icons.rounded.Visibility
|
||||
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
|
|
@ -74,7 +73,6 @@ import androidx.compose.material3.OutlinedTextField
|
|||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -110,7 +108,6 @@ import org.meshtastic.core.model.util.getChannel
|
|||
import org.meshtastic.core.service.RetryEvent
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.alert_bell_text
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.cancel_reply
|
||||
import org.meshtastic.core.strings.clear_selection
|
||||
import org.meshtastic.core.strings.copy
|
||||
|
|
@ -135,6 +132,7 @@ import org.meshtastic.core.strings.send
|
|||
import org.meshtastic.core.strings.type_a_message
|
||||
import org.meshtastic.core.strings.unknown
|
||||
import org.meshtastic.core.strings.unknown_channel
|
||||
import org.meshtastic.core.ui.component.MeshtasticTextDialog
|
||||
import org.meshtastic.core.ui.component.NodeKeyStatusIcon
|
||||
import org.meshtastic.core.ui.component.SecurityIcon
|
||||
import org.meshtastic.core.ui.component.SharedContactDialog
|
||||
|
|
@ -241,11 +239,6 @@ fun MessageScreen(
|
|||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val lastReadMessageTimestamp by
|
||||
remember(contactKey, contactSettings) {
|
||||
derivedStateOf { contactSettings[contactKey]?.lastReadMessageTimestamp }
|
||||
}
|
||||
|
||||
// Track unread messages using lightweight metadata queries
|
||||
val hasUnreadMessages by viewModel.hasUnreadMessages(contactKey).collectAsStateWithLifecycle(initialValue = false)
|
||||
val firstUnreadMessageUuid by
|
||||
|
|
@ -307,7 +300,11 @@ fun MessageScreen(
|
|||
is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum)
|
||||
MessageScreenEvent.NavigateBack -> onNavigateBack()
|
||||
is MessageScreenEvent.CopyToClipboard -> {
|
||||
clipboardManager.nativeClipboard.setPrimaryClip(ClipData.newPlainText(event.text, event.text))
|
||||
coroutineScope.launch {
|
||||
clipboardManager.setClipEntry(
|
||||
androidx.compose.ui.platform.ClipEntry(ClipData.newPlainText(event.text, event.text)),
|
||||
)
|
||||
}
|
||||
selectedMessageIds.value = emptySet()
|
||||
}
|
||||
}
|
||||
|
|
@ -644,13 +641,12 @@ private fun String.limitBytes(maxBytes: Int): String {
|
|||
private fun DeleteMessageDialog(count: Int, onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||
val deleteMessagesString = pluralStringResource(Res.plurals.delete_messages, count, count)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
title = { Text(stringResource(Res.string.delete_messages_title)) },
|
||||
text = { Text(text = deleteMessagesString) },
|
||||
confirmButton = { TextButton(onClick = onConfirm) { Text(stringResource(Res.string.delete)) } },
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(Res.string.cancel)) } },
|
||||
MeshtasticTextDialog(
|
||||
titleRes = Res.string.delete_messages_title,
|
||||
message = deleteMessagesString,
|
||||
confirmTextRes = Res.string.delete,
|
||||
onConfirm = onConfirm,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,9 @@
|
|||
*/
|
||||
package org.meshtastic.feature.messaging
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -38,8 +36,6 @@ import androidx.compose.material.icons.rounded.Add
|
|||
import androidx.compose.material.icons.rounded.DragHandle
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.FastForward
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -63,8 +59,6 @@ import androidx.compose.ui.focus.FocusRequester
|
|||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
|
|
@ -84,6 +78,7 @@ import org.meshtastic.core.strings.quick_chat_instant
|
|||
import org.meshtastic.core.strings.quick_chat_new
|
||||
import org.meshtastic.core.strings.save
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.component.dragContainer
|
||||
import org.meshtastic.core.ui.component.dragDropItemsIndexed
|
||||
import org.meshtastic.core.ui.component.rememberDragDropState
|
||||
|
|
@ -185,20 +180,17 @@ private fun EditQuickChatDialog(
|
|||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
MeshtasticDialog(
|
||||
onDismiss = onDismiss,
|
||||
title = stringResource(title),
|
||||
confirmText = stringResource(Res.string.save),
|
||||
onConfirm = {
|
||||
onSave(actionInput)
|
||||
onDismiss()
|
||||
},
|
||||
dismissText = stringResource(Res.string.cancel),
|
||||
text = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = stringResource(title),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style =
|
||||
MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextFieldWithCounter(
|
||||
|
|
@ -257,39 +249,19 @@ private fun EditQuickChatDialog(
|
|||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
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))
|
||||
}
|
||||
|
||||
if (!newQuickChat) {
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
onDelete(actionInput)
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(Res.string.delete))
|
||||
Text(text = stringResource(Res.string.delete), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
onSave(actionInput)
|
||||
onDismiss()
|
||||
},
|
||||
enabled = actionInput.name.isNotEmpty() && actionInput.message.isNotEmpty(),
|
||||
) {
|
||||
Text(text = stringResource(Res.string.save))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ package org.meshtastic.feature.messaging.component
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -42,6 +40,7 @@ import org.meshtastic.core.strings.retry_dialog_confirm
|
|||
import org.meshtastic.core.strings.retry_dialog_message
|
||||
import org.meshtastic.core.strings.retry_dialog_reaction_message
|
||||
import org.meshtastic.core.strings.retry_dialog_title
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
|
||||
private const val COUNTDOWN_DELAY_MS = 1000L
|
||||
private const val MESSAGE_PREVIEW_LENGTH = 50
|
||||
|
|
@ -120,22 +119,13 @@ fun RetryConfirmationDialog(
|
|||
onTimeout()
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { /* Prevent dismissal by clicking outside */ },
|
||||
dismissButton = {
|
||||
FilledTonalButton(onClick = onCancel) { Text(text = stringResource(Res.string.retry_dialog_cancel)) }
|
||||
},
|
||||
confirmButton = {
|
||||
FilledTonalButton(onClick = onConfirm) { Text(text = stringResource(Res.string.retry_dialog_confirm)) }
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(Res.string.retry_dialog_title),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
},
|
||||
MeshtasticDialog(
|
||||
onDismiss = onCancel,
|
||||
dismissText = stringResource(Res.string.retry_dialog_cancel),
|
||||
confirmText = stringResource(Res.string.retry_dialog_confirm),
|
||||
onConfirm = onConfirm,
|
||||
title = stringResource(Res.string.retry_dialog_title),
|
||||
text = { RetryDialogContent(retryEvent, timeRemaining) },
|
||||
dismissable = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,10 +41,6 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.graphics.Color
|
||||
|
|
@ -66,13 +62,6 @@ import org.meshtastic.feature.node.model.MetricsState
|
|||
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||
|
||||
private enum class DialogType {
|
||||
FAVORITE,
|
||||
IGNORE,
|
||||
MUTE,
|
||||
REMOVE,
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeviceActions(
|
||||
node: Node,
|
||||
|
|
@ -84,38 +73,13 @@ fun DeviceActions(
|
|||
modifier: Modifier = Modifier,
|
||||
isLocal: Boolean = false,
|
||||
) {
|
||||
var displayedDialog by remember { mutableStateOf<DialogType?>(null) }
|
||||
|
||||
NodeActionDialogs(
|
||||
node = node,
|
||||
displayFavoriteDialog = displayedDialog == DialogType.FAVORITE,
|
||||
displayIgnoreDialog = displayedDialog == DialogType.IGNORE,
|
||||
displayMuteDialog = displayedDialog == DialogType.MUTE,
|
||||
displayRemoveDialog = displayedDialog == DialogType.REMOVE,
|
||||
onDismissMenuRequest = { displayedDialog = null },
|
||||
onConfirmFavorite = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Favorite(it))) },
|
||||
onConfirmIgnore = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Ignore(it))) },
|
||||
onConfirmMute = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Mute(it))) },
|
||||
onConfirmRemove = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Remove(it))) },
|
||||
)
|
||||
|
||||
Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
SectionCard(title = Res.string.actions) {
|
||||
PrimaryActionsRow(
|
||||
node = node,
|
||||
isLocal = isLocal,
|
||||
onAction = onAction,
|
||||
onFavoriteClick = { displayedDialog = DialogType.FAVORITE },
|
||||
)
|
||||
PrimaryActionsRow(node = node, isLocal = isLocal, onAction = onAction)
|
||||
|
||||
if (!isLocal) {
|
||||
SectionDivider(Modifier.padding(vertical = 8.dp))
|
||||
ManagementActions(
|
||||
node = node,
|
||||
onIgnoreClick = { displayedDialog = DialogType.IGNORE },
|
||||
onMuteClick = { displayedDialog = DialogType.MUTE },
|
||||
onRemoveClick = { displayedDialog = DialogType.REMOVE },
|
||||
)
|
||||
ManagementActions(node = node, onAction = onAction)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,12 +96,7 @@ fun DeviceActions(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun PrimaryActionsRow(
|
||||
node: Node,
|
||||
isLocal: Boolean,
|
||||
onAction: (NodeDetailAction) -> Unit,
|
||||
onFavoriteClick: () -> Unit,
|
||||
) {
|
||||
private fun PrimaryActionsRow(node: Node, isLocal: Boolean, onAction: (NodeDetailAction) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 20.dp).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
|
|
@ -173,7 +132,10 @@ private fun PrimaryActionsRow(
|
|||
}
|
||||
|
||||
if (!isLocal) {
|
||||
IconToggleButton(checked = node.isFavorite, onCheckedChange = { onFavoriteClick() }) {
|
||||
IconToggleButton(
|
||||
checked = node.isFavorite,
|
||||
onCheckedChange = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Favorite(node))) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (node.isFavorite) Icons.Rounded.Star else Icons.Rounded.StarBorder,
|
||||
contentDescription = stringResource(Res.string.favorite),
|
||||
|
|
@ -185,12 +147,7 @@ private fun PrimaryActionsRow(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ManagementActions(
|
||||
node: Node,
|
||||
onIgnoreClick: () -> Unit,
|
||||
onMuteClick: () -> Unit,
|
||||
onRemoveClick: () -> Unit,
|
||||
) {
|
||||
private fun ManagementActions(node: Node, onAction: (NodeDetailAction) -> Unit) {
|
||||
Column {
|
||||
SwitchListItem(
|
||||
text = stringResource(Res.string.ignore),
|
||||
|
|
@ -201,7 +158,7 @@ private fun ManagementActions(
|
|||
Icons.AutoMirrored.Default.VolumeUp
|
||||
},
|
||||
checked = node.isIgnored,
|
||||
onClick = onIgnoreClick,
|
||||
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Ignore(node))) },
|
||||
)
|
||||
|
||||
if (node.capabilities.canMuteNode) {
|
||||
|
|
@ -214,7 +171,7 @@ private fun ManagementActions(
|
|||
Icons.AutoMirrored.Default.VolumeUp
|
||||
},
|
||||
checked = node.isMuted,
|
||||
onClick = onMuteClick,
|
||||
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Mute(node))) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -224,7 +181,7 @@ private fun ManagementActions(
|
|||
trailingIcon = null,
|
||||
textColor = MaterialTheme.colorScheme.error,
|
||||
leadingIconTint = MaterialTheme.colorScheme.error,
|
||||
onClick = onRemoveClick,
|
||||
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Remove(node))) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,133 +0,0 @@
|
|||
/*
|
||||
* 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.feature.node.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.favorite
|
||||
import org.meshtastic.core.strings.favorite_add
|
||||
import org.meshtastic.core.strings.favorite_remove
|
||||
import org.meshtastic.core.strings.ignore
|
||||
import org.meshtastic.core.strings.ignore_add
|
||||
import org.meshtastic.core.strings.ignore_remove
|
||||
import org.meshtastic.core.strings.mute_add
|
||||
import org.meshtastic.core.strings.mute_notifications
|
||||
import org.meshtastic.core.strings.mute_remove
|
||||
import org.meshtastic.core.strings.remove
|
||||
import org.meshtastic.core.strings.remove_node_text
|
||||
import org.meshtastic.core.strings.unmute
|
||||
import org.meshtastic.core.ui.component.SimpleAlertDialog
|
||||
|
||||
@Composable
|
||||
fun NodeActionDialogs(
|
||||
node: Node,
|
||||
displayFavoriteDialog: Boolean,
|
||||
displayIgnoreDialog: Boolean,
|
||||
displayMuteDialog: Boolean,
|
||||
displayRemoveDialog: Boolean,
|
||||
onDismissMenuRequest: () -> Unit,
|
||||
onConfirmFavorite: (Node) -> Unit,
|
||||
onConfirmIgnore: (Node) -> Unit,
|
||||
onConfirmMute: (Node) -> Unit,
|
||||
onConfirmRemove: (Node) -> Unit,
|
||||
) {
|
||||
if (displayFavoriteDialog) {
|
||||
SimpleAlertDialog(
|
||||
title = Res.string.favorite,
|
||||
text =
|
||||
stringResource(
|
||||
if (node.isFavorite) Res.string.favorite_remove else Res.string.favorite_add,
|
||||
node.user.long_name ?: "",
|
||||
),
|
||||
onConfirm = {
|
||||
onDismissMenuRequest()
|
||||
onConfirmFavorite(node)
|
||||
},
|
||||
onDismiss = onDismissMenuRequest,
|
||||
)
|
||||
}
|
||||
if (displayIgnoreDialog) {
|
||||
SimpleAlertDialog(
|
||||
title = Res.string.ignore,
|
||||
text =
|
||||
stringResource(
|
||||
if (node.isIgnored) Res.string.ignore_remove else Res.string.ignore_add,
|
||||
node.user.long_name ?: "",
|
||||
),
|
||||
onConfirm = {
|
||||
onDismissMenuRequest()
|
||||
onConfirmIgnore(node)
|
||||
},
|
||||
onDismiss = onDismissMenuRequest,
|
||||
)
|
||||
}
|
||||
if (displayMuteDialog) {
|
||||
SimpleAlertDialog(
|
||||
title = if (node.isMuted) Res.string.unmute else Res.string.mute_notifications,
|
||||
text =
|
||||
stringResource(
|
||||
if (node.isMuted) Res.string.mute_remove else Res.string.mute_add,
|
||||
node.user.long_name ?: "",
|
||||
),
|
||||
onConfirm = {
|
||||
onDismissMenuRequest()
|
||||
onConfirmMute(node)
|
||||
},
|
||||
onDismiss = onDismissMenuRequest,
|
||||
)
|
||||
}
|
||||
if (displayRemoveDialog) {
|
||||
SimpleAlertDialog(
|
||||
title = Res.string.remove,
|
||||
text = stringResource(Res.string.remove_node_text),
|
||||
onConfirm = {
|
||||
onDismissMenuRequest()
|
||||
onConfirmRemove(node)
|
||||
},
|
||||
onDismiss = onDismissMenuRequest,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class NodeMenuAction {
|
||||
data class Remove(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Ignore(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Mute(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Favorite(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class DirectMessage(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class RequestUserInfo(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class RequestNeighborInfo(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class RequestPosition(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class RequestTelemetry(val node: Node, val type: TelemetryType) : NodeMenuAction()
|
||||
|
||||
data class TraceRoute(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class MoreDetails(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Share(val node: Node) : NodeMenuAction()
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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.feature.node.component
|
||||
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
|
||||
sealed class NodeMenuAction {
|
||||
data class Remove(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Ignore(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Mute(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Favorite(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class DirectMessage(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class RequestUserInfo(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class RequestNeighborInfo(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class RequestPosition(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class RequestTelemetry(val node: Node, val type: TelemetryType) : NodeMenuAction()
|
||||
|
||||
data class TraceRoute(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class MoreDetails(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Share(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Reboot(val node: Node) : NodeMenuAction()
|
||||
|
||||
data class Shutdown(val node: Node) : NodeMenuAction()
|
||||
}
|
||||
|
|
@ -281,10 +281,10 @@ constructor(
|
|||
|
||||
fun handleNodeMenuAction(action: NodeMenuAction) {
|
||||
when (action) {
|
||||
is NodeMenuAction.Remove -> nodeManagementActions.removeNode(viewModelScope, action.node.num)
|
||||
is NodeMenuAction.Ignore -> nodeManagementActions.ignoreNode(viewModelScope, action.node)
|
||||
is NodeMenuAction.Mute -> nodeManagementActions.muteNode(viewModelScope, action.node)
|
||||
is NodeMenuAction.Favorite -> nodeManagementActions.favoriteNode(viewModelScope, action.node)
|
||||
is NodeMenuAction.Remove -> nodeManagementActions.requestRemoveNode(viewModelScope, action.node)
|
||||
is NodeMenuAction.Ignore -> nodeManagementActions.requestIgnoreNode(viewModelScope, action.node)
|
||||
is NodeMenuAction.Mute -> nodeManagementActions.requestMuteNode(viewModelScope, action.node)
|
||||
is NodeMenuAction.Favorite -> nodeManagementActions.requestFavoriteNode(viewModelScope, action.node)
|
||||
is NodeMenuAction.RequestUserInfo ->
|
||||
nodeRequestActions.requestUserInfo(viewModelScope, action.node.num, action.node.user.long_name ?: "")
|
||||
is NodeMenuAction.RequestNeighborInfo ->
|
||||
|
|
|
|||
|
|
@ -21,10 +21,25 @@ import co.touchlab.kermit.Logger
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.favorite
|
||||
import org.meshtastic.core.strings.favorite_add
|
||||
import org.meshtastic.core.strings.favorite_remove
|
||||
import org.meshtastic.core.strings.ignore
|
||||
import org.meshtastic.core.strings.ignore_add
|
||||
import org.meshtastic.core.strings.ignore_remove
|
||||
import org.meshtastic.core.strings.mute_add
|
||||
import org.meshtastic.core.strings.mute_notifications
|
||||
import org.meshtastic.core.strings.mute_remove
|
||||
import org.meshtastic.core.strings.remove
|
||||
import org.meshtastic.core.strings.remove_node_text
|
||||
import org.meshtastic.core.strings.unmute
|
||||
import org.meshtastic.core.ui.util.AlertManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -34,7 +49,16 @@ class NodeManagementActions
|
|||
constructor(
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val alertManager: AlertManager,
|
||||
) {
|
||||
fun requestRemoveNode(scope: CoroutineScope, node: Node) {
|
||||
alertManager.showAlert(
|
||||
titleRes = Res.string.remove,
|
||||
messageRes = Res.string.remove_node_text,
|
||||
onConfirm = { removeNode(scope, node.num) },
|
||||
)
|
||||
}
|
||||
|
||||
fun removeNode(scope: CoroutineScope, nodeNum: Int) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Logger.i { "Removing node '$nodeNum'" }
|
||||
|
|
@ -48,6 +72,21 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun requestIgnoreNode(scope: CoroutineScope, node: Node) {
|
||||
scope.launch {
|
||||
val message =
|
||||
getString(
|
||||
if (node.isIgnored) Res.string.ignore_remove else Res.string.ignore_add,
|
||||
node.user.long_name ?: "",
|
||||
)
|
||||
alertManager.showAlert(
|
||||
titleRes = Res.string.ignore,
|
||||
message = message,
|
||||
onConfirm = { ignoreNode(scope, node) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun ignoreNode(scope: CoroutineScope, node: Node) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
@ -58,6 +97,18 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun requestMuteNode(scope: CoroutineScope, node: Node) {
|
||||
scope.launch {
|
||||
val message =
|
||||
getString(if (node.isMuted) Res.string.mute_remove else Res.string.mute_add, node.user.long_name ?: "")
|
||||
alertManager.showAlert(
|
||||
titleRes = if (node.isMuted) Res.string.unmute else Res.string.mute_notifications,
|
||||
message = message,
|
||||
onConfirm = { muteNode(scope, node) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun muteNode(scope: CoroutineScope, node: Node) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
@ -68,6 +119,21 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun requestFavoriteNode(scope: CoroutineScope, node: Node) {
|
||||
scope.launch {
|
||||
val message =
|
||||
getString(
|
||||
if (node.isFavorite) Res.string.favorite_remove else Res.string.favorite_add,
|
||||
node.user.long_name ?: "",
|
||||
)
|
||||
alertManager.showAlert(
|
||||
titleRes = Res.string.favorite,
|
||||
message = message,
|
||||
onConfirm = { favoriteNode(scope, node) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun favoriteNode(scope: CoroutineScope, node: Node) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* 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.feature.node.list
|
||||
|
||||
import android.os.RemoteException
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class NodeActions
|
||||
@Inject
|
||||
constructor(
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
) {
|
||||
suspend fun favoriteNode(node: Node) {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e(ex) { "Favorite node error" }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun ignoreNode(node: Node) {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Ignore(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e(ex) { "Ignore node error" }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun muteNode(node: Node) {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Mute(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e(ex) { "Mute node error" }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeNode(nodeNum: Int) = withContext(Dispatchers.IO) {
|
||||
Logger.i { "Removing node '$nodeNum'" }
|
||||
try {
|
||||
val packetId = serviceRepository.meshService?.packetId ?: return@withContext
|
||||
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
|
||||
nodeRepository.deleteNode(nodeNum)
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e { "Remove node error: ${ex.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -80,14 +80,13 @@ import org.meshtastic.core.strings.remove
|
|||
import org.meshtastic.core.strings.remove_favorite
|
||||
import org.meshtastic.core.strings.remove_ignored
|
||||
import org.meshtastic.core.strings.unmute
|
||||
import org.meshtastic.core.ui.component.AddContactFAB
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.MeshtasticImportFAB
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.component.smartScrollToTop
|
||||
import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.feature.node.component.NodeActionDialogs
|
||||
import org.meshtastic.feature.node.component.NodeFilterTextField
|
||||
import org.meshtastic.feature.node.component.NodeItem
|
||||
import org.meshtastic.proto.SharedContact
|
||||
|
|
@ -149,17 +148,18 @@ fun NodeListScreen(
|
|||
floatingActionButton = {
|
||||
val shareCapable = ourNode?.capabilities?.supportsQrCodeSharing ?: false
|
||||
val sharedContact: SharedContact? by viewModel.sharedContactRequested.collectAsStateWithLifecycle(null)
|
||||
AddContactFAB(
|
||||
MeshtasticImportFAB(
|
||||
sharedContact = sharedContact,
|
||||
modifier =
|
||||
Modifier.animateFloatingActionButton(
|
||||
visible = !isScrollInProgress && connectionState == ConnectionState.Connected && shareCapable,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
onResult = { uri ->
|
||||
onImport = { uri ->
|
||||
viewModel.handleScannedUri(uri) { scope.launch { context.showToast(Res.string.channel_invalid) } }
|
||||
},
|
||||
onDismissSharedContact = { viewModel.setSharedContactRequested(null) },
|
||||
isContactContext = true,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
|
|
@ -195,29 +195,6 @@ fun NodeListScreen(
|
|||
}
|
||||
|
||||
items(nodes, key = { it.num }) { node ->
|
||||
var displayFavoriteDialog by remember { mutableStateOf(false) }
|
||||
var displayIgnoreDialog by remember { mutableStateOf(false) }
|
||||
var displayMuteDialog by remember { mutableStateOf(false) }
|
||||
var displayRemoveDialog by remember { mutableStateOf(false) }
|
||||
|
||||
NodeActionDialogs(
|
||||
node = node,
|
||||
displayFavoriteDialog = displayFavoriteDialog,
|
||||
displayIgnoreDialog = displayIgnoreDialog,
|
||||
displayMuteDialog = displayMuteDialog,
|
||||
displayRemoveDialog = displayRemoveDialog,
|
||||
onDismissMenuRequest = {
|
||||
displayFavoriteDialog = false
|
||||
displayIgnoreDialog = false
|
||||
displayMuteDialog = false
|
||||
displayRemoveDialog = false
|
||||
},
|
||||
onConfirmFavorite = viewModel::favoriteNode,
|
||||
onConfirmIgnore = viewModel::ignoreNode,
|
||||
onConfirmMute = viewModel::muteNode,
|
||||
onConfirmRemove = { viewModel.removeNode(it.num) },
|
||||
)
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
|
|
@ -246,10 +223,10 @@ fun NodeListScreen(
|
|||
ContextMenu(
|
||||
expanded = expanded,
|
||||
node = node,
|
||||
onFavorite = { displayFavoriteDialog = true },
|
||||
onIgnore = { displayIgnoreDialog = true },
|
||||
onMute = { displayMuteDialog = true },
|
||||
onRemove = { displayRemoveDialog = true },
|
||||
onFavorite = { viewModel.favoriteNode(node) },
|
||||
onIgnore = { viewModel.ignoreNode(node) },
|
||||
onMute = { viewModel.muteNode(node) },
|
||||
onRemove = { viewModel.removeNode(node) },
|
||||
onDismiss = { expanded = false },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ import org.meshtastic.core.data.repository.NodeRepository
|
|||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.model.util.toChannelSet
|
||||
import org.meshtastic.core.model.util.dispatchMeshtasticUri
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.component.toSharedContact
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.node.detail.NodeManagementActions
|
||||
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.Config
|
||||
|
|
@ -53,7 +53,7 @@ constructor(
|
|||
private val nodeRepository: NodeRepository,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
val nodeActions: NodeActions,
|
||||
val nodeManagementActions: NodeManagementActions,
|
||||
val nodeFilterPreferences: NodeFilterPreferences,
|
||||
) : ViewModel() {
|
||||
|
||||
|
|
@ -164,20 +164,13 @@ constructor(
|
|||
_sharedContactRequested.value = sharedContact
|
||||
}
|
||||
|
||||
/** Unified handler for scanned Meshtastic URIs (contacts or channels). */
|
||||
fun handleScannedUri(uri: Uri, onInvalid: () -> Unit) {
|
||||
if (uri.path?.contains("/v/") == true) {
|
||||
runCatching { _sharedContactRequested.value = uri.toSharedContact() }
|
||||
.onFailure { ex ->
|
||||
Logger.e(ex) { "Shared contact error" }
|
||||
onInvalid()
|
||||
}
|
||||
} else {
|
||||
runCatching { _requestChannelSet.value = uri.toChannelSet() }
|
||||
.onFailure { ex ->
|
||||
Logger.e(ex) { "Channel url error" }
|
||||
onInvalid()
|
||||
}
|
||||
}
|
||||
uri.dispatchMeshtasticUri(
|
||||
onContact = { _sharedContactRequested.value = it },
|
||||
onChannel = { _requestChannelSet.value = it },
|
||||
onInvalid = onInvalid,
|
||||
)
|
||||
}
|
||||
|
||||
fun clearRequestChannelSet() {
|
||||
|
|
@ -196,13 +189,13 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun favoriteNode(node: Node) = viewModelScope.launch { nodeActions.favoriteNode(node) }
|
||||
fun favoriteNode(node: Node) = nodeManagementActions.requestFavoriteNode(viewModelScope, node)
|
||||
|
||||
fun ignoreNode(node: Node) = viewModelScope.launch { nodeActions.ignoreNode(node) }
|
||||
fun ignoreNode(node: Node) = nodeManagementActions.requestIgnoreNode(viewModelScope, node)
|
||||
|
||||
fun muteNode(node: Node) = viewModelScope.launch { nodeActions.muteNode(node) }
|
||||
fun muteNode(node: Node) = nodeManagementActions.requestMuteNode(viewModelScope, node)
|
||||
|
||||
fun removeNode(nodeNum: Int) = viewModelScope.launch { nodeActions.removeNode(nodeNum) }
|
||||
fun removeNode(node: Node) = nodeManagementActions.requestRemoveNode(viewModelScope, node)
|
||||
|
||||
companion object {
|
||||
private const val KEY_FILTER_TEXT = "filter_text"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ package org.meshtastic.feature.node.metrics
|
|||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
|
@ -32,11 +35,13 @@ import kotlinx.coroutines.flow.asFlow
|
|||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.data.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
||||
|
|
@ -56,6 +61,11 @@ import org.meshtastic.core.service.ServiceAction
|
|||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.fallback_node_name
|
||||
import org.meshtastic.core.strings.okay
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.strings.view_on_map
|
||||
import org.meshtastic.core.ui.util.AlertManager
|
||||
import org.meshtastic.core.ui.util.toMessageRes
|
||||
import org.meshtastic.core.ui.util.toPosition
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
|
|
@ -96,6 +106,7 @@ constructor(
|
|||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
private val nodeRequestActions: NodeRequestActions,
|
||||
private val alertManager: AlertManager,
|
||||
) : ViewModel() {
|
||||
private var destNum: Int? =
|
||||
runCatching { savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum }.getOrNull()
|
||||
|
|
@ -230,6 +241,52 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun showLogDetail(titleRes: StringResource, annotatedMessage: AnnotatedString) {
|
||||
alertManager.showAlert(
|
||||
titleRes = titleRes,
|
||||
composableMessage = { SelectionContainer { Text(text = annotatedMessage) } },
|
||||
)
|
||||
}
|
||||
|
||||
fun showTracerouteDetail(
|
||||
annotatedMessage: AnnotatedString,
|
||||
requestId: Int,
|
||||
responseLogUuid: String,
|
||||
overlay: TracerouteOverlay?,
|
||||
onViewOnMap: (Int, String) -> Unit,
|
||||
onShowError: (StringResource) -> Unit,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val snapshotPositions = tracerouteSnapshotRepository.getSnapshotPositions(responseLogUuid).first()
|
||||
alertManager.showAlert(
|
||||
titleRes = Res.string.traceroute,
|
||||
composableMessage = { SelectionContainer { Text(text = annotatedMessage) } },
|
||||
confirmTextRes = Res.string.view_on_map,
|
||||
onConfirm = {
|
||||
val positionedNodeNums =
|
||||
if (snapshotPositions.isNotEmpty()) {
|
||||
snapshotPositions.keys
|
||||
} else {
|
||||
positionedNodeNums()
|
||||
}
|
||||
val availability =
|
||||
evaluateTracerouteMapAvailability(
|
||||
forwardRoute = overlay?.forwardRoute.orEmpty(),
|
||||
returnRoute = overlay?.returnRoute.orEmpty(),
|
||||
positionedNodeNums = positionedNodeNums,
|
||||
)
|
||||
val errorRes = availability.toMessageRes()
|
||||
if (errorRes != null) {
|
||||
onShowError(errorRes)
|
||||
} else {
|
||||
onViewOnMap(requestId, responseLogUuid)
|
||||
}
|
||||
},
|
||||
dismissTextRes = Res.string.okay,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
initializeFlows()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,12 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
|
|
@ -41,13 +39,7 @@ 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.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
|
@ -58,9 +50,6 @@ import org.meshtastic.core.strings.Res
|
|||
import org.meshtastic.core.strings.neighbor_info
|
||||
import org.meshtastic.core.strings.routing_error_no_response
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.SNR_FAIR_THRESHOLD
|
||||
import org.meshtastic.core.ui.component.SNR_GOOD_THRESHOLD
|
||||
import org.meshtastic.core.ui.component.SimpleAlertDialog
|
||||
import org.meshtastic.core.ui.icon.Groups
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.PersonOff
|
||||
|
|
@ -68,6 +57,7 @@ import org.meshtastic.core.ui.icon.Refresh
|
|||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
|
||||
import org.meshtastic.core.ui.util.annotateNeighborInfo
|
||||
import org.meshtastic.feature.node.component.CooldownIconButton
|
||||
import org.meshtastic.feature.node.detail.NodeRequestEffect
|
||||
|
||||
|
|
@ -96,22 +86,12 @@ fun NeighborInfoLogScreen(
|
|||
fun getUsername(nodeNum: Int): String =
|
||||
with(viewModel.getUser(nodeNum)) { "${long_name ?: ""} (${short_name ?: ""})" }
|
||||
|
||||
var showDialog by remember { mutableStateOf<AnnotatedString?>(null) }
|
||||
val context = LocalContext.current
|
||||
|
||||
val statusGreen = MaterialTheme.colorScheme.StatusGreen
|
||||
val statusYellow = MaterialTheme.colorScheme.StatusYellow
|
||||
val statusOrange = MaterialTheme.colorScheme.StatusOrange
|
||||
|
||||
showDialog?.let { message ->
|
||||
SimpleAlertDialog(
|
||||
title = Res.string.neighbor_info,
|
||||
text = { SelectionContainer { Text(text = message) } },
|
||||
onConfirm = { showDialog = null },
|
||||
onDismiss = { showDialog = null },
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val lastRequestNeighborsTime by viewModel.lastRequestNeighborsTime.collectAsState()
|
||||
|
|
@ -174,13 +154,14 @@ fun NeighborInfoLogScreen(
|
|||
header = getString(Res.string.neighbor_info),
|
||||
)
|
||||
?.let {
|
||||
showDialog =
|
||||
val message =
|
||||
annotateNeighborInfo(
|
||||
it,
|
||||
statusGreen = statusGreen,
|
||||
statusYellow = statusYellow,
|
||||
statusOrange = statusOrange,
|
||||
)
|
||||
viewModel.showLogDetail(Res.string.neighbor_info, message)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -195,43 +176,3 @@ fun NeighborInfoLogScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a raw neighbor info string into an [AnnotatedString] with SNR values highlighted according to their quality.
|
||||
*/
|
||||
fun annotateNeighborInfo(
|
||||
inString: String?,
|
||||
statusGreen: Color,
|
||||
statusYellow: Color,
|
||||
statusOrange: Color,
|
||||
): AnnotatedString {
|
||||
if (inString == null) return buildAnnotatedString { append("") }
|
||||
return buildAnnotatedString {
|
||||
inString.lines().forEachIndexed { i, line ->
|
||||
if (i > 0) append("\n")
|
||||
// Example line: "• NodeName (SNR: 5.5)"
|
||||
if (line.contains("(SNR: ")) {
|
||||
val snrRegex = Regex("""\(SNR: ([\d.?-]+)\)""")
|
||||
val snrMatch = snrRegex.find(line)
|
||||
val snrValue = snrMatch?.groupValues?.getOrNull(1)?.toFloatOrNull()
|
||||
|
||||
if (snrValue != null) {
|
||||
val snrColor =
|
||||
when {
|
||||
snrValue >= SNR_GOOD_THRESHOLD -> statusGreen
|
||||
snrValue >= SNR_FAIR_THRESHOLD -> statusYellow
|
||||
else -> statusOrange
|
||||
}
|
||||
val snrPrefix = "(SNR: "
|
||||
append(line.substring(0, line.indexOf(snrPrefix) + snrPrefix.length))
|
||||
withStyle(style = SpanStyle(color = snrColor, fontWeight = FontWeight.Bold)) { append("$snrValue") }
|
||||
append(")")
|
||||
} else {
|
||||
append(line)
|
||||
}
|
||||
} else {
|
||||
append(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,12 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
|
|
@ -44,23 +42,17 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.meshtastic.core.strings.getString
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.pluralStringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
|
||||
import org.meshtastic.core.model.fullRouteDiscovery
|
||||
import org.meshtastic.core.model.getTracerouteResponse
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.close
|
||||
import org.meshtastic.core.strings.routing_error_no_response
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.strings.traceroute_diff
|
||||
|
|
@ -71,11 +63,7 @@ import org.meshtastic.core.strings.traceroute_log
|
|||
import org.meshtastic.core.strings.traceroute_route_back_to_us
|
||||
import org.meshtastic.core.strings.traceroute_route_towards_dest
|
||||
import org.meshtastic.core.strings.traceroute_time_and_text
|
||||
import org.meshtastic.core.strings.view_on_map
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.SNR_FAIR_THRESHOLD
|
||||
import org.meshtastic.core.ui.component.SNR_GOOD_THRESHOLD
|
||||
import org.meshtastic.core.ui.component.SimpleAlertDialog
|
||||
import org.meshtastic.core.ui.icon.Group
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.PersonOff
|
||||
|
|
@ -85,21 +73,13 @@ import org.meshtastic.core.ui.theme.AppTheme
|
|||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
|
||||
import org.meshtastic.core.ui.util.toMessageRes
|
||||
import org.meshtastic.core.ui.util.annotateTraceroute
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
import org.meshtastic.feature.node.component.CooldownIconButton
|
||||
import org.meshtastic.feature.node.detail.NodeRequestEffect
|
||||
import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.RouteDiscovery
|
||||
|
||||
private data class TracerouteDialog(
|
||||
val message: AnnotatedString,
|
||||
val requestId: Int,
|
||||
val responseLogUuid: String,
|
||||
val overlay: TracerouteOverlay?,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
|
|
@ -126,19 +106,10 @@ fun TracerouteLogScreen(
|
|||
fun getUsername(nodeNum: Int): String =
|
||||
with(viewModel.getUser(nodeNum)) { "${long_name ?: ""} (${short_name ?: ""})" }
|
||||
|
||||
var showDialog by remember { mutableStateOf<TracerouteDialog?>(null) }
|
||||
var errorMessageRes by remember { mutableStateOf<StringResource?>(null) }
|
||||
val context = LocalContext.current
|
||||
|
||||
TracerouteLogDialogs(
|
||||
dialog = showDialog,
|
||||
errorMessageRes = errorMessageRes,
|
||||
viewModel = viewModel,
|
||||
onViewOnMap = onViewOnMap,
|
||||
onShowErrorMessageRes = { errorMessageRes = it },
|
||||
onDismissDialog = { showDialog = null },
|
||||
onDismissError = { errorMessageRes = null },
|
||||
)
|
||||
val statusGreen = MaterialTheme.colorScheme.StatusGreen
|
||||
val statusYellow = MaterialTheme.colorScheme.StatusYellow
|
||||
val statusOrange = MaterialTheme.colorScheme.StatusOrange
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
|
|
@ -199,6 +170,9 @@ fun TracerouteLogScreen(
|
|||
headerTowards = stringResource(Res.string.traceroute_route_towards_dest),
|
||||
headerBack = stringResource(Res.string.traceroute_route_back_to_us),
|
||||
),
|
||||
statusGreen = statusGreen,
|
||||
statusYellow = statusYellow,
|
||||
statusOrange = statusOrange,
|
||||
)
|
||||
val durationText = stringResource(Res.string.traceroute_duration, "%.1f".format(seconds))
|
||||
buildAnnotatedString {
|
||||
|
|
@ -242,16 +216,24 @@ fun TracerouteLogScreen(
|
|||
headerTowards = getString(Res.string.traceroute_route_towards_dest),
|
||||
headerBack = getString(Res.string.traceroute_route_back_to_us),
|
||||
)
|
||||
?.let { AnnotatedString(it) }
|
||||
?.let {
|
||||
annotateTraceroute(
|
||||
it,
|
||||
statusGreen = statusGreen,
|
||||
statusYellow = statusYellow,
|
||||
statusOrange = statusOrange,
|
||||
)
|
||||
}
|
||||
dialogMessage?.let {
|
||||
val responseLogUuid = result?.uuid ?: return@combinedClickable
|
||||
showDialog =
|
||||
TracerouteDialog(
|
||||
message = it,
|
||||
requestId = log.fromRadio.packet?.id ?: 0,
|
||||
responseLogUuid = responseLogUuid,
|
||||
overlay = overlay,
|
||||
)
|
||||
viewModel.showTracerouteDetail(
|
||||
annotatedMessage = it,
|
||||
requestId = log.fromRadio.packet?.id ?: 0,
|
||||
responseLogUuid = responseLogUuid,
|
||||
overlay = overlay,
|
||||
onViewOnMap = onViewOnMap,
|
||||
onShowError = { /* Handle error */ },
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -267,55 +249,6 @@ fun TracerouteLogScreen(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TracerouteLogDialogs(
|
||||
dialog: TracerouteDialog?,
|
||||
errorMessageRes: StringResource?,
|
||||
viewModel: MetricsViewModel,
|
||||
onViewOnMap: (requestId: Int, responseLogUuid: String) -> Unit,
|
||||
onShowErrorMessageRes: (StringResource) -> Unit,
|
||||
onDismissDialog: () -> Unit,
|
||||
onDismissError: () -> Unit,
|
||||
) {
|
||||
dialog?.let { dialogState ->
|
||||
val snapshotPositionsFlow =
|
||||
remember(dialogState.responseLogUuid) { viewModel.tracerouteSnapshotPositions(dialogState.responseLogUuid) }
|
||||
val snapshotPositions by snapshotPositionsFlow.collectAsStateWithLifecycle(emptyMap<Int, Position>())
|
||||
SimpleAlertDialog(
|
||||
title = Res.string.traceroute,
|
||||
text = { SelectionContainer { Text(text = dialogState.message) } },
|
||||
confirmText = stringResource(Res.string.view_on_map),
|
||||
onConfirm = {
|
||||
val positionedNodeNums =
|
||||
if (snapshotPositions.isNotEmpty()) {
|
||||
snapshotPositions.keys
|
||||
} else {
|
||||
viewModel.positionedNodeNums()
|
||||
}
|
||||
val availability =
|
||||
evaluateTracerouteMapAvailability(
|
||||
forwardRoute = dialogState.overlay?.forwardRoute.orEmpty(),
|
||||
returnRoute = dialogState.overlay?.returnRoute.orEmpty(),
|
||||
positionedNodeNums = positionedNodeNums,
|
||||
)
|
||||
availability.toMessageRes()?.let(onShowErrorMessageRes)
|
||||
?: onViewOnMap(dialogState.requestId, dialogState.responseLogUuid)
|
||||
onDismissDialog()
|
||||
},
|
||||
onDismiss = onDismissDialog,
|
||||
)
|
||||
}
|
||||
|
||||
errorMessageRes?.let { res ->
|
||||
SimpleAlertDialog(
|
||||
title = Res.string.traceroute,
|
||||
text = { Text(text = stringResource(res)) },
|
||||
dismissText = stringResource(Res.string.close),
|
||||
onDismiss = onDismissError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Generates a display string and icon based on the route discovery information. */
|
||||
@Composable
|
||||
private fun RouteDiscovery?.getTextAndIcon(): Pair<String, ImageVector> = when {
|
||||
|
|
@ -340,44 +273,6 @@ private fun RouteDiscovery?.getTextAndIcon(): Pair<String, ImageVector> = when {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a raw traceroute string into an [AnnotatedString] with SNR values highlighted according to their quality.
|
||||
*
|
||||
* @param inString The raw string output from a traceroute response.
|
||||
* @return An [AnnotatedString] with SNR values styled, or an empty [AnnotatedString] if input is null.
|
||||
*/
|
||||
@Composable
|
||||
fun annotateTraceroute(inString: String?): AnnotatedString {
|
||||
if (inString == null) return buildAnnotatedString { append("") }
|
||||
return buildAnnotatedString {
|
||||
inString.lines().forEachIndexed { i, line ->
|
||||
if (i > 0) append("\n")
|
||||
// Example line: "⇊ -8.75 dB SNR"
|
||||
if (line.trimStart().startsWith("⇊")) {
|
||||
val snrRegex = Regex("""⇊ ([\d\.\?-]+) dB""")
|
||||
val snrMatch = snrRegex.find(line)
|
||||
val snrValue = snrMatch?.groupValues?.getOrNull(1)?.toFloatOrNull()
|
||||
|
||||
if (snrValue != null) {
|
||||
val snrColor =
|
||||
when {
|
||||
snrValue >= SNR_GOOD_THRESHOLD -> MaterialTheme.colorScheme.StatusGreen
|
||||
snrValue >= SNR_FAIR_THRESHOLD -> MaterialTheme.colorScheme.StatusYellow
|
||||
else -> MaterialTheme.colorScheme.StatusOrange
|
||||
}
|
||||
withStyle(style = SpanStyle(color = snrColor, fontWeight = FontWeight.Bold)) { append(line) }
|
||||
} else {
|
||||
// Append line as is if SNR value cannot be parsed
|
||||
append(line)
|
||||
}
|
||||
} else {
|
||||
// Append non-SNR lines as is
|
||||
append(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun TracerouteItemPreview() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.feature.node.detail
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.util.AlertManager
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NodeManagementActionsTest {
|
||||
|
||||
private val nodeRepository = mockk<NodeRepository>(relaxed = true)
|
||||
private val serviceRepository = mockk<ServiceRepository>(relaxed = true)
|
||||
private val alertManager = mockk<AlertManager>(relaxed = true)
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private val testScope = TestScope(testDispatcher)
|
||||
|
||||
private val actions =
|
||||
NodeManagementActions(
|
||||
nodeRepository = nodeRepository,
|
||||
serviceRepository = serviceRepository,
|
||||
alertManager = alertManager,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `requestRemoveNode shows confirmation alert`() {
|
||||
val node = Node(num = 123, user = User(long_name = "Test Node"))
|
||||
|
||||
actions.requestRemoveNode(testScope, node)
|
||||
|
||||
verify {
|
||||
alertManager.showAlert(
|
||||
titleRes = any(),
|
||||
messageRes = any(),
|
||||
onConfirm = any(),
|
||||
onDismiss = any(),
|
||||
confirmText = any(),
|
||||
confirmTextRes = any(),
|
||||
dismissText = any(),
|
||||
dismissTextRes = any(),
|
||||
choices = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `requestFavoriteNode shows confirmation alert`() = runTest(testDispatcher) {
|
||||
// This test might fail due to getString() not being mocked easily
|
||||
// but let's see if we can at least get requestRemoveNode passing.
|
||||
// Actually, if getString() fails, the coroutine will fail.
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)) } },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue