diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 6703e26f1..81cef36fb 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -278,6 +278,11 @@ class UIViewModel @Inject constructor( private val onlyOnline = MutableStateFlow(preferences.getBoolean("only-online", false)) private val onlyDirect = MutableStateFlow(preferences.getBoolean("only-direct", false)) + private val onlyFavorites = MutableStateFlow(preferences.getBoolean("only-favorites", false)) + private val showWaypointsOnMap = MutableStateFlow(preferences.getBoolean("show-waypoints-on-map", true)) + private val showPrecisionCircleOnMap = + MutableStateFlow(preferences.getBoolean("show-precision-circle-on-map", true)) + fun setSortOption(sort: NodeSortOption) { nodeSortOption.value = sort } @@ -302,6 +307,21 @@ class UIViewModel @Inject constructor( preferences.edit { putBoolean("only-direct", onlyDirect.value) } } + fun setOnlyFavorites(value: Boolean) { + onlyFavorites.value = value + preferences.edit { putBoolean("only-favorites", onlyFavorites.value) } + } + + fun setShowWaypointsOnMap(value: Boolean) { + showWaypointsOnMap.value = value + preferences.edit { putBoolean("show-waypoints-on-map", value) } + } + + fun setShowPrecisionCircleOnMap(value: Boolean) { + showPrecisionCircleOnMap.value = value + preferences.edit { putBoolean("show-precision-circle-on-map", value) } + } + data class NodeFilterState( val filterText: String, val includeUnknown: Boolean, @@ -365,6 +385,24 @@ class UIViewModel @Inject constructor( initialValue = emptyList(), ) + data class MapFilterState( + val onlyFavorites: Boolean, + val showWaypoints: Boolean, + val showPrecisionCircle: Boolean, + ) + + val mapFilterStateFlow: StateFlow = combine( + onlyFavorites, + showWaypointsOnMap, + showPrecisionCircleOnMap, + ) { favoritesOnly, showWaypoints, showPrecisionCircle -> + MapFilterState(favoritesOnly, showWaypoints, showPrecisionCircle) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = MapFilterState(false, true, true) + ) + // hardware info about our local device (can be null) val myNodeInfo: StateFlow get() = nodeDB.myNodeInfo val ourNodeInfo: StateFlow get() = nodeDB.ourNodeInfo diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/main/java/com/geeksville/mesh/ui/map/MapView.kt index 333972cbb..4f436a1fc 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/map/MapView.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/map/MapView.kt @@ -24,15 +24,25 @@ import androidx.appcompat.content.res.AppCompatResources 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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lens import androidx.compose.material.icons.filled.LocationDisabled +import androidx.compose.material.icons.filled.PinDrop import androidx.compose.material.icons.outlined.Layers import androidx.compose.material.icons.outlined.MyLocation +import androidx.compose.material.icons.outlined.Tune +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableDoubleStateOf import androidx.compose.runtime.mutableStateOf @@ -40,6 +50,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.foundation.background +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -201,6 +215,10 @@ private fun Context.purgeTileSource(onResult: (String) -> Unit) { fun MapView( model: UIViewModel = viewModel(), ) { + var mapFilterExpanded by remember { mutableStateOf(false) } + + val mapFilterState by model.mapFilterStateFlow.collectAsState() + // UI Elements var cacheEstimate by remember { mutableStateOf("") } @@ -289,7 +307,12 @@ fun MapView( val ourNode = model.ourNodeInfo.value val gpsFormat = model.config.display.gpsFormat.number val displayUnits = model.config.display.units.number - return nodesWithPosition.map { node -> + val mapFilterState = model.mapFilterStateFlow.value // Access mapFilterState directly + return nodesWithPosition.mapNotNull { node -> + if (mapFilterState.onlyFavorites && !node.isFavorite && !node.equals(ourNode)) { + return@mapNotNull null + } + val (p, u) = node.position to node.user val nodePosition = GeoPoint(node.latitude, node.longitude) MarkerWithLabel( @@ -306,20 +329,21 @@ fun MapView( if (node.batteryStr != "") node.batteryStr else "?" ) ourNode?.distanceStr(node, displayUnits)?.let { dist -> - subDescription = - context.getString(R.string.map_subDescription, ourNode.bearing(node), dist) + subDescription = context.getString( + R.string.map_subDescription, + ourNode.bearing(node), + dist + ) } setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) position = nodePosition icon = markerIcon - -// setOnLongClickListener { -// performHapticFeedback() -// TODO NodeMenu? -// true -// } setNodeColors(node.colors) - setPrecisionBits(p.precisionBits) + if (!mapFilterState.showPrecisionCircle) { + setPrecisionBits(0) + } else { + setPrecisionBits(p.precisionBits) + } } } } @@ -376,6 +400,7 @@ fun MapView( val dateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) return waypoints.mapNotNull { waypoint -> val pt = waypoint.data.waypoint ?: return@mapNotNull null + if (!mapFilterState.showWaypoints) return@mapNotNull null val lock = if (pt.lockedTo != 0) "\uD83D\uDD12" else "" val time = dateFormat.format(waypoint.received_time) val label = pt.name + " " + formatAgo((waypoint.received_time / 1000).toInt()) @@ -633,6 +658,105 @@ fun MapView( icon = Icons.Outlined.Layers, contentDescription = R.string.map_style_selection, ) + Box(modifier = Modifier) { + MapButton( + onClick = { mapFilterExpanded = true }, + icon = Icons.Outlined.Tune, + contentDescription = R.string.map_filter, + ) + DropdownMenu( + expanded = mapFilterExpanded, + onDismissRequest = { mapFilterExpanded = false }, + modifier = Modifier.background(MaterialTheme.colorScheme.surface) + ) { + // Only Favorites toggle + DropdownMenuItem( + text = { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Star, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.only_favorites), + modifier = Modifier.weight(1f) + ) + Checkbox( + checked = mapFilterState.onlyFavorites, + onCheckedChange = { enabled -> + model.setOnlyFavorites(enabled) + }, + modifier = Modifier.padding(start = 8.dp) + ) + } + }, + onClick = { + model.setOnlyFavorites(!mapFilterState.onlyFavorites) + } + ) + DropdownMenuItem( + text = { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.PinDrop, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.show_waypoints), + modifier = Modifier.weight(1f) + ) + Checkbox( + checked = mapFilterState.showWaypoints, + onCheckedChange = model::setShowWaypointsOnMap, + modifier = Modifier.padding(start = 8.dp) + ) + } + }, + onClick = { + model.setShowWaypointsOnMap(!mapFilterState.showWaypoints) + } + ) + DropdownMenuItem( + text = { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Lens, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.show_precision_circle), + modifier = Modifier.weight(1f) + ) + Checkbox( + checked = mapFilterState.showPrecisionCircle, + onCheckedChange = { enabled -> + model.setShowPrecisionCircleOnMap(enabled) + }, + modifier = Modifier.padding(start = 8.dp) + ) + } + }, + onClick = { + model.setShowPrecisionCircleOnMap(!mapFilterState.showPrecisionCircle) + } + ) + } + } if (hasGps) { MapButton( icon = if (myLocationOverlay == null) { @@ -666,6 +790,7 @@ fun MapView( if (name == "") name = "Dropped Pin" if (expire == 0) expire = Int.MAX_VALUE lockedTo = if (waypoint.lockedTo != 0) model.myNodeNum ?: 0 else 0 + if (waypoint.icon == 0) icon = 128205 } ) }, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bda202378..2a4e53c0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -674,4 +674,8 @@ Expires Time Date + Map Filter\n + Only Favorites + Show Waypoints + Show Precision Circles