mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Added map filter + small waypoint fix (#2065)
Co-authored-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
67b7ccfe06
commit
e7657c4763
3 changed files with 177 additions and 10 deletions
|
|
@ -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<MapFilterState> = 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<MyNodeEntity?> get() = nodeDB.myNodeInfo
|
||||
val ourNodeInfo: StateFlow<Node?> get() = nodeDB.ourNodeInfo
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -674,4 +674,8 @@
|
|||
<string name="expires">Expires</string>
|
||||
<string name="time">Time</string>
|
||||
<string name="date">Date</string>
|
||||
<string name="map_filter">Map Filter\n</string>
|
||||
<string name="only_favorites">Only Favorites</string>
|
||||
<string name="show_waypoints">Show Waypoints</string>
|
||||
<string name="show_precision_circle">Show Precision Circles</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue