Remove node card expansion (#3243)

This commit is contained in:
Phil Oliver 2025-09-29 16:54:28 -04:00 committed by GitHub
parent 9ed0f5e0a7
commit a878373d14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 22 additions and 138 deletions

View file

@ -91,7 +91,9 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD
val currentTimeMillis = rememberTimeTickWithLifecycle()
val connectionState by nodesViewModel.connectionState.collectAsStateWithLifecycle()
val isScrollInProgress by remember { derivedStateOf { listState.isScrollInProgress } }
val isScrollInProgress by remember {
derivedStateOf { listState.isScrollInProgress && (listState.canScrollForward || listState.canScrollBackward) }
}
Scaffold(
topBar = {
MainAppBar(
@ -144,8 +146,6 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD
onToggleOnlyOnline = nodesViewModel::toggleOnlyOnline,
onlyDirect = state.filter.onlyDirect,
onToggleOnlyDirect = nodesViewModel::toggleOnlyDirect,
showDetails = state.showDetails,
onToggleShowDetails = nodesViewModel::toggleShowDetails,
showIgnored = state.filter.showIgnored,
onToggleShowIgnored = nodesViewModel::toggleShowIgnored,
ignoredNodeCount = ignoredNodeCount,
@ -172,8 +172,14 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD
onConfirmRemove = { nodesViewModel.removeNode(it.num) },
)
Box {
Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
var showContextMenu by remember { mutableStateOf(false) }
val longClick =
if (node.num != ourNode?.num) {
{ showContextMenu = true }
} else {
null
}
NodeItem(
modifier = Modifier.animateItem(),
@ -181,9 +187,8 @@ fun NodeScreen(nodesViewModel: NodesViewModel = hiltViewModel(), navigateToNodeD
thatNode = node,
distanceUnits = state.distanceUnits,
tempInFahrenheit = state.tempInFahrenheit,
onClickChip = { navigateToNodeDetails(it.num) },
onLongClick = { showContextMenu = true },
expanded = state.showDetails,
onClick = { navigateToNodeDetails(node.num) },
onLongClick = longClick,
currentTimeMillis = currentTimeMillis,
isConnected = connectionState.isConnected(),
)
@ -213,7 +218,7 @@ private fun ContextMenu(
onClickRemove: (Node) -> Unit,
onDismiss: () -> Unit,
) {
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss, offset = DpOffset(16.dp, 0.dp)) {
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss, offset = DpOffset(x = 0.dp, y = 8.dp)) {
val isFavorite = node.isFavorite
val isIgnored = node.isIgnored

View file

@ -34,14 +34,12 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
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.datastore.UiPreferencesDataSource
import org.meshtastic.core.prefs.ui.UiPrefs
import timber.log.Timber
import javax.inject.Inject
@ -52,7 +50,6 @@ constructor(
private val nodeRepository: NodeRepository,
radioConfigRepository: RadioConfigRepository,
private val serviceRepository: ServiceRepository,
private val uiPrefs: UiPrefs,
private val uiPreferencesDataSource: UiPreferencesDataSource,
) : ViewModel() {
@ -97,21 +94,13 @@ constructor(
NodeFilterState(filterText, includeUnknown, onlyOnline, onlyDirect, showIgnored)
}
private val showDetails = MutableStateFlow(uiPrefs.showDetails)
val nodesUiState: StateFlow<NodesUiState> =
combine(nodeSortOption, nodeFilter, showDetails, radioConfigRepository.deviceProfileFlow) {
sort,
nodeFilter,
showDetails,
profile,
->
combine(nodeSortOption, nodeFilter, radioConfigRepository.deviceProfileFlow) { sort, nodeFilter, profile ->
NodesUiState(
sort = sort,
filter = nodeFilter,
distanceUnits = profile.config.display.units.number,
tempInFahrenheit = profile.moduleConfig.telemetry.environmentDisplayFahrenheit,
showDetails = showDetails,
)
}
.stateIn(
@ -172,8 +161,6 @@ constructor(
uiPreferencesDataSource.setNodeSort(sort.ordinal)
}
fun toggleShowDetails() = toggle(showDetails) { uiPrefs.showDetails = it }
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.AddSharedContact(sharedContact)) }
@ -181,13 +168,6 @@ constructor(
_sharedContactRequested.value = sharedContact
}
private fun toggle(state: MutableStateFlow<Boolean>, onChanged: (newValue: Boolean) -> Unit) {
(!state.value).let { toggled ->
state.update { toggled }
onChanged(toggled)
}
}
fun favoriteNode(node: Node) = viewModelScope.launch {
try {
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
@ -221,7 +201,6 @@ data class NodesUiState(
val filter: NodeFilterState = NodeFilterState(),
val distanceUnits: Int = 0,
val tempInFahrenheit: Boolean = false,
val showDetails: Boolean = false,
)
data class NodeFilterState(

View file

@ -75,8 +75,6 @@ fun NodeFilterTextField(
onToggleOnlyOnline: () -> Unit,
onlyDirect: Boolean,
onToggleOnlyDirect: () -> Unit,
showDetails: Boolean,
onToggleShowDetails: () -> Unit,
showIgnored: Boolean,
onToggleShowIgnored: () -> Unit,
ignoredNodeCount: Int,
@ -97,8 +95,6 @@ fun NodeFilterTextField(
onToggleOnlyOnline = onToggleOnlyOnline,
onlyDirect = onlyDirect,
onToggleOnlyDirect = onToggleOnlyDirect,
showDetails = showDetails,
onToggleShowDetails = onToggleShowDetails,
showIgnored = showIgnored,
onToggleShowIgnored = onToggleShowIgnored,
ignoredNodeCount = ignoredNodeCount,
@ -258,25 +254,6 @@ private fun NodeSortButton(
},
)
HorizontalDivider()
DropdownMenuItem(
onClick = {
toggles.onToggleShowDetails()
expanded = false
},
text = {
Row {
AnimatedVisibility(visible = toggles.showDetails) {
Icon(
imageVector = Icons.Default.Done,
contentDescription = null,
modifier = Modifier.padding(end = 4.dp),
)
}
Text(text = stringResource(id = R.string.node_filter_show_details))
}
},
)
HorizontalDivider()
DropdownMenuItem(
onClick = {
toggles.onToggleShowIgnored()
@ -321,8 +298,6 @@ private fun NodeFilterTextFieldPreview() {
onToggleOnlyOnline = {},
onlyDirect = false,
onToggleOnlyDirect = {},
showDetails = false,
onToggleShowDetails = {},
showIgnored = false,
onToggleShowIgnored = {},
ignoredNodeCount = 0,
@ -337,8 +312,6 @@ data class NodeFilterToggles(
val onToggleOnlyOnline: () -> Unit,
val onlyDirect: Boolean,
val onToggleOnlyDirect: () -> Unit,
val showDetails: Boolean,
val onToggleShowDetails: () -> Unit,
val showIgnored: Boolean,
val onToggleShowIgnored: () -> Unit,
val ignoredNodeCount: Int,

View file

@ -31,27 +31,22 @@ 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.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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.ui.common.components.SignalInfo
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
import org.meshtastic.core.database.model.Node
@ -69,9 +64,8 @@ fun NodeItem(
distanceUnits: Int,
tempInFahrenheit: Boolean,
modifier: Modifier = Modifier,
onClickChip: (Node) -> Unit = {},
onLongClick: () -> Unit = {},
expanded: Boolean = false,
onClick: () -> Unit = {},
onLongClick: (() -> Unit)? = null,
currentTimeMillis: Long,
isConnected: Boolean = false,
) {
@ -83,18 +77,6 @@ fun NodeItem(
val distance =
remember(thisNode, thatNode) { thisNode?.distance(thatNode)?.takeIf { it > 0 }?.toDistanceString(system) }
val hwInfoString =
when (val hwModel = thatNode.user.hwModel) {
MeshProtos.HardwareModel.UNSET -> MeshProtos.HardwareModel.UNSET.name
else -> hwModel.name.replace('_', '-').replace('p', '.').lowercase()
}
val roleName =
if (thatNode.isUnknownUser) {
DeviceConfig.Role.UNRECOGNIZED.name
} else {
thatNode.user.role.name
}
val style =
if (thatNode.isUnknownUser) {
LocalTextStyle.current.copy(fontStyle = FontStyle.Italic)
@ -114,7 +96,6 @@ fun NodeItem(
.copy(containerColor = containerColor, contentColor = contentColorFor(containerColor))
} ?: (CardDefaults.cardColors())
val (detailsShown, showDetails) = remember { mutableStateOf(expanded) }
val unmessageable =
remember(thatNode) {
when {
@ -123,18 +104,13 @@ fun NodeItem(
}
}
Card(
modifier =
modifier
.combinedClickable(onClick = { showDetails(!detailsShown) }, onLongClick = onLongClick)
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.defaultMinSize(minHeight = 80.dp),
colors = cardColors,
) {
Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
Card(modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 80.dp), colors = cardColors) {
Column(
modifier =
Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick).fillMaxWidth().padding(8.dp),
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
NodeChip(node = thatNode, onClick = onClickChip)
NodeChip(node = thatNode)
NodeKeyStatusIcon(
hasPKC = thatNode.hasPKC,
@ -190,51 +166,6 @@ fun NodeItem(
)
}
}
if (detailsShown || expanded) {
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
thatNode.validPosition?.let {
LinkedCoordinates(
latitude = thatNode.latitude,
longitude = thatNode.longitude,
nodeName = longName,
)
}
thatNode.validPosition?.let { position ->
ElevationInfo(
altitude = position.altitude,
system = system,
suffix = stringResource(id = R.string.elevation_suffix),
)
}
}
Spacer(modifier = Modifier.height(4.dp))
Row(modifier = Modifier.fillMaxWidth()) {
Text(
modifier = Modifier.weight(1f),
text = hwInfoString,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
style = style,
)
Text(
modifier = Modifier.weight(1f),
text = roleName,
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
style = style,
)
Text(
modifier = Modifier.weight(1f),
text = thatNode.user.id.ifEmpty { "???" },
textAlign = TextAlign.End,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
style = style,
)
}
}
}
}
}
@ -261,7 +192,6 @@ fun NodeInfoPreview(@PreviewParameter(NodePreviewParameterProvider::class) thatN
thatNode = thatNode,
distanceUnits = 1,
tempInFahrenheit = true,
expanded = false,
currentTimeMillis = System.currentTimeMillis(),
)
Text(text = "Details Shown", color = MaterialTheme.colorScheme.onBackground)
@ -270,7 +200,6 @@ fun NodeInfoPreview(@PreviewParameter(NodePreviewParameterProvider::class) thatN
thatNode = thatNode,
distanceUnits = 1,
tempInFahrenheit = true,
expanded = true,
currentTimeMillis = System.currentTimeMillis(),
)
}