kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
Context menu tweaks
rodzic
1512cd8b64
commit
530665f368
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue