Context menu tweaks

pull/3220/head
Phil Oliver 2025-09-28 21:12:11 -04:00
rodzic 1512cd8b64
commit 530665f368
3 zmienionych plików z 136 dodań i 99 usunięć

Wyświetl plik

@ -29,31 +29,49 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DoDisturbOn
import androidx.compose.material.icons.outlined.DoDisturbOn
import androidx.compose.material.icons.rounded.DeleteOutline
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarBorder
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
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
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.node.components.NodeActionDialogs
import com.geeksville.mesh.ui.node.components.NodeFilterTextField
import com.geeksville.mesh.ui.node.components.NodeItem
import com.geeksville.mesh.ui.sharing.AddContactFAB
import com.geeksville.mesh.ui.sharing.supportsQrCodeSharing
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.rememberTimeTickWithLifecycle
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -135,23 +153,124 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD
}
items(nodes, key = { it.num }) { node ->
NodeItem(
modifier = Modifier.animateItem(),
thisNode = ourNode,
thatNode = node,
distanceUnits = state.distanceUnits,
tempInFahrenheit = state.tempInFahrenheit,
onClickChip = { navigateToNodeDetails(it.num) },
expanded = state.showDetails,
currentTimeMillis = currentTimeMillis,
isConnected = connectionState.isConnected(),
var displayFavoriteDialog by remember { mutableStateOf(false) }
var displayIgnoreDialog by remember { mutableStateOf(false) }
var displayRemoveDialog by remember { mutableStateOf(false) }
NodeActionDialogs(
node = node,
displayFavoriteDialog = displayFavoriteDialog,
displayIgnoreDialog = displayIgnoreDialog,
displayRemoveDialog = displayRemoveDialog,
onDismissMenuRequest = {
displayFavoriteDialog = false
displayIgnoreDialog = false
displayRemoveDialog = false
},
onConfirmFavorite = nodesViewModel::favoriteNode,
onConfirmIgnore = nodesViewModel::ignoreNode,
onConfirmRemove = { nodesViewModel.removeNode(it.num) },
)
Box {
var showContextMenu by remember { mutableStateOf(false) }
NodeItem(
modifier = Modifier.animateItem(),
thisNode = ourNode,
thatNode = node,
distanceUnits = state.distanceUnits,
tempInFahrenheit = state.tempInFahrenheit,
onClickChip = { navigateToNodeDetails(it.num) },
onLongClick = { showContextMenu = true },
expanded = state.showDetails,
currentTimeMillis = currentTimeMillis,
isConnected = connectionState.isConnected(),
)
val isThisNode = remember(node) { ourNode?.num == node.num }
ContextMenu(
expanded = !isThisNode && showContextMenu,
node = node,
onClickFavorite = { displayFavoriteDialog = true },
onClickIgnore = { displayIgnoreDialog = true },
onClickRemove = { displayRemoveDialog = true },
onDismiss = { showContextMenu = false },
)
}
}
item { Spacer(modifier = Modifier.height(88.dp)) }
}
}
}
}
@Composable
private fun ContextMenu(
expanded: Boolean,
node: Node,
onClickFavorite: (Node) -> Unit,
onClickIgnore: (Node) -> Unit,
onClickRemove: (Node) -> Unit,
onDismiss: () -> Unit,
) {
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss, offset = DpOffset(16.dp, 0.dp)) {
val isFavorite = node.isFavorite
val isIgnored = node.isIgnored
DropdownMenuItem(
onClick = {
onClickFavorite(node)
onDismiss()
},
enabled = !isIgnored,
leadingIcon = {
Icon(
imageVector = if (isFavorite) Icons.Rounded.Star else Icons.Rounded.StarBorder,
contentDescription = null,
)
},
text = { Text(stringResource(if (isFavorite) R.string.remove_favorite else R.string.add_favorite)) },
)
DropdownMenuItem(
onClick = {
onClickIgnore(node)
onDismiss()
},
leadingIcon = {
Icon(
imageVector = if (isIgnored) Icons.Filled.DoDisturbOn else Icons.Outlined.DoDisturbOn,
contentDescription = null,
tint = MaterialTheme.colorScheme.StatusRed,
)
},
text = {
Text(
text = stringResource(if (isIgnored) R.string.remove_ignored else R.string.ignore),
color = MaterialTheme.colorScheme.StatusRed,
)
},
)
DropdownMenuItem(
onClick = {
onClickRemove(node)
onDismiss()
},
enabled = !isIgnored,
leadingIcon = {
Icon(
imageVector = Icons.Rounded.DeleteOutline,
contentDescription = null,
tint = if (isIgnored) LocalContentColor.current else MaterialTheme.colorScheme.StatusRed,
)
},
text = {
Text(
text = stringResource(R.string.remove),
color = if (isIgnored) Color.Unspecified else MaterialTheme.colorScheme.StatusRed,
)
},
)
}
}

Wyświetl plik

@ -18,10 +18,8 @@
package com.geeksville.mesh.ui.node.components
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -33,18 +31,14 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
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
@ -76,9 +70,7 @@ fun NodeItem(
tempInFahrenheit: Boolean,
modifier: Modifier = Modifier,
onClickChip: (Node) -> Unit = {},
onConfirmFavorite: (Node) -> Unit,
onConfirmIgnore: (Node) -> Unit,
onConfirmRemove: (Node) -> Unit,
onLongClick: () -> Unit = {},
expanded: Boolean = false,
currentTimeMillis: Long,
isConnected: Boolean = false,
@ -131,31 +123,10 @@ fun NodeItem(
}
}
var displayFavoriteDialog by remember { mutableStateOf(false) }
var displayIgnoreDialog by remember { mutableStateOf(false) }
var displayRemoveDialog by remember { mutableStateOf(false) }
NodeActionDialogs(
node = thatNode,
displayFavoriteDialog = displayFavoriteDialog,
displayIgnoreDialog = displayIgnoreDialog,
displayRemoveDialog = displayRemoveDialog,
onDismissMenuRequest = {
displayFavoriteDialog = false
displayIgnoreDialog = false
displayRemoveDialog = false
},
onConfirmFavorite = onConfirmFavorite,
onConfirmIgnore = onConfirmIgnore,
onConfirmRemove = onConfirmRemove,
)
var showContextMenu by remember { mutableStateOf(false) }
Card(
modifier =
modifier
.combinedClickable(onClick = { showDetails(!detailsShown) }, onLongClick = { showContextMenu = true })
.combinedClickable(onClick = { showDetails(!detailsShown) }, onLongClick = onLongClick)
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.defaultMinSize(minHeight = 80.dp),
@ -163,22 +134,7 @@ fun NodeItem(
) {
Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box {
NodeChip(node = thatNode, onClick = onClickChip)
ContextMenu(
expanded = !isThisNode && showContextMenu,
node = thatNode,
onConfirm = { action, _ ->
when (action) {
ContextMenuAction.Favorite -> displayFavoriteDialog = true
ContextMenuAction.Ignore -> displayIgnoreDialog = true
ContextMenuAction.Remove -> displayRemoveDialog = true
}
},
onDismiss = { showContextMenu = false },
)
}
NodeChip(node = thatNode, onClick = onClickChip)
NodeKeyStatusIcon(
hasPKC = thatNode.hasPKC,
@ -289,16 +245,7 @@ fun NodeInfoSimplePreview() {
AppTheme {
val thisNode = NodePreviewParameterProvider().values.first()
val thatNode = NodePreviewParameterProvider().values.last()
NodeItem(
thisNode = thisNode,
thatNode = thatNode,
0,
true,
currentTimeMillis = System.currentTimeMillis(),
onConfirmFavorite = {},
onConfirmIgnore = {},
onConfirmRemove = {},
)
NodeItem(thisNode = thisNode, thatNode = thatNode, 0, true, currentTimeMillis = System.currentTimeMillis())
}
}
@ -316,9 +263,6 @@ fun NodeInfoPreview(@PreviewParameter(NodePreviewParameterProvider::class) thatN
tempInFahrenheit = true,
expanded = false,
currentTimeMillis = System.currentTimeMillis(),
onConfirmFavorite = {},
onConfirmIgnore = {},
onConfirmRemove = {},
)
Text(text = "Details Shown", color = MaterialTheme.colorScheme.onBackground)
NodeItem(
@ -328,36 +272,7 @@ fun NodeInfoPreview(@PreviewParameter(NodePreviewParameterProvider::class) thatN
tempInFahrenheit = true,
expanded = true,
currentTimeMillis = System.currentTimeMillis(),
onConfirmFavorite = {},
onConfirmIgnore = {},
onConfirmRemove = {},
)
}
}
}
@Composable
private fun ContextMenu(
expanded: Boolean,
node: Node,
onConfirm: (ContextMenuAction, Node) -> Unit,
onDismiss: () -> Unit,
) {
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
ContextMenuAction.entries.forEach {
DropdownMenuItem(
text = { Text(stringResource(it.text)) },
onClick = {
onConfirm(it, node)
onDismiss()
},
)
}
}
}
internal enum class ContextMenuAction(@StringRes val text: Int) {
Favorite(R.string.favorite),
Ignore(R.string.ignore),
Remove(R.string.remove),
}

Wyświetl plik

@ -286,6 +286,7 @@
<string name="delivery_confirmed">Delivery confirmed</string>
<string name="error">Error</string>
<string name="ignore">Ignore</string>
<string name="remove_ignored">Remove from ignored</string>
<string name="ignore_add">Add \'%s\' to ignore list?</string>
<string name="ignore_remove">Remove \'%s\' from ignore list?</string>
<string name="map_select_download_region">Select download region</string>
@ -386,6 +387,8 @@
<string name="alert_bell_text">Alert Bell Character!</string>
<string name="critical_alert">Critical Alert!</string>
<string name="favorite">Favorite</string>
<string name="add_favorite">Add to favorites</string>
<string name="remove_favorite">Remove from favorites</string>
<string name="favorite_add">Add \'%s\' as a favorite node?</string>
<string name="favorite_remove">Remove \'%s\' as a favorite node?</string>
<string name="power_metrics_log">Power Metrics Log</string>