feat(map): add last heard filter for map nodes (#3219)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
pull/3222/head
James Rich 2025-09-27 13:40:41 -05:00 zatwierdzone przez GitHub
rodzic ab18e424b1
commit 61c6d6c76e
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
5 zmienionych plików z 98 dodań i 14 usunięć

Wyświetl plik

@ -301,11 +301,13 @@ fun MapView(
val displayableWaypoints = waypoints.values.mapNotNull { it.data.waypoint }
val filteredNodes =
if (mapFilterState.onlyFavorites) {
allNodes.filter { it.isFavorite || it.num == ourNodeInfo?.num }
} else {
allNodes
}
allNodes
.filter { node -> !mapFilterState.onlyFavorites || node.isFavorite || node.num == ourNodeInfo?.num }
.filter { node ->
mapFilterState.lastHeardFilter.seconds == 0L ||
(System.currentTimeMillis() / 1000 - node.lastHeard) <= mapFilterState.lastHeardFilter.seconds ||
node.num == ourNodeInfo?.num
}
val nodeClusterItems =
filteredNodes.map { node ->

Wyświetl plik

@ -17,21 +17,33 @@
package com.geeksville.mesh.ui.map.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Place
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.RadioButtonUnchecked
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ui.map.LastHeardFilter
import com.geeksville.mesh.ui.map.MapViewModel
import org.meshtastic.core.strings.R
import kotlin.math.roundToInt
@Composable
internal fun MapFilterDropdown(expanded: Boolean, onDismissRequest: () -> Unit, mapViewModel: MapViewModel) {
@ -41,10 +53,7 @@ internal fun MapFilterDropdown(expanded: Boolean, onDismissRequest: () -> Unit,
text = { Text(stringResource(id = R.string.only_favorites)) },
onClick = { mapViewModel.toggleOnlyFavorites() },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Favorite,
contentDescription = stringResource(id = R.string.only_favorites),
)
Icon(imageVector = Icons.Filled.Star, contentDescription = stringResource(id = R.string.only_favorites))
},
trailingIcon = {
Checkbox(
@ -85,5 +94,30 @@ internal fun MapFilterDropdown(expanded: Boolean, onDismissRequest: () -> Unit,
)
},
)
HorizontalDivider()
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
val filterOptions = LastHeardFilter.entries
val selectedIndex = filterOptions.indexOf(mapFilterState.lastHeardFilter)
var sliderPosition by remember(selectedIndex) { mutableFloatStateOf(selectedIndex.toFloat()) }
Text(
text =
stringResource(
R.string.last_heard_filter_label,
stringResource(mapFilterState.lastHeardFilter.label),
),
style = MaterialTheme.typography.labelLarge,
)
Slider(
value = sliderPosition,
onValueChange = { sliderPosition = it },
onValueChangeFinished = {
val newIndex = sliderPosition.roundToInt().coerceIn(0, filterOptions.size - 1)
mapViewModel.setLastHeardFilter(filterOptions[newIndex])
},
valueRange = 0f..(filterOptions.size - 1).toFloat(),
steps = filterOptions.size - 2,
)
}
}
}

Wyświetl plik

@ -18,6 +18,7 @@
package com.geeksville.mesh.ui.map
import android.os.RemoteException
import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.MeshProtos
@ -37,7 +38,28 @@ import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.strings.R
import timber.log.Timber
import java.util.concurrent.TimeUnit
@Suppress("MagicNumber")
sealed class LastHeardFilter(val seconds: Long, @StringRes val label: Int) {
data object Any : LastHeardFilter(0L, R.string.any)
data object OneHour : LastHeardFilter(TimeUnit.HOURS.toSeconds(1), R.string.one_hour)
data object EightHours : LastHeardFilter(TimeUnit.HOURS.toSeconds(8), R.string.eight_hours)
data object OneDay : LastHeardFilter(TimeUnit.DAYS.toSeconds(1), R.string.one_day)
data object TwoDays : LastHeardFilter(TimeUnit.DAYS.toSeconds(2), R.string.two_days)
companion object {
fun fromSeconds(seconds: Long): LastHeardFilter = entries.find { it.seconds == seconds } ?: Any
val entries = listOf(Any, OneHour, EightHours, OneDay, TwoDays)
}
}
@Suppress("TooManyFunctions")
abstract class BaseMapViewModel(
@ -80,6 +102,13 @@ abstract class BaseMapViewModel(
private val showPrecisionCircleOnMap = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap)
private val lastHeardFilter = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter))
fun setLastHeardFilter(filter: LastHeardFilter) {
mapPrefs.lastHeardFilter = filter.seconds
lastHeardFilter.value = filter
}
val ourNodeInfo: StateFlow<Node?> = nodeRepository.ourNodeInfo
val isConnected =
@ -133,20 +162,31 @@ abstract class BaseMapViewModel(
}
}
data class MapFilterState(val onlyFavorites: Boolean, val showWaypoints: Boolean, val showPrecisionCircle: Boolean)
data class MapFilterState(
val onlyFavorites: Boolean,
val showWaypoints: Boolean,
val showPrecisionCircle: Boolean,
val lastHeardFilter: LastHeardFilter,
)
val mapFilterStateFlow: StateFlow<MapFilterState> =
combine(showOnlyFavorites, showWaypointsOnMap, showPrecisionCircleOnMap) {
combine(showOnlyFavorites, showWaypointsOnMap, showPrecisionCircleOnMap, lastHeardFilter) {
favoritesOnly,
showWaypoints,
showPrecisionCircle,
lastHeard,
->
MapFilterState(favoritesOnly, showWaypoints, showPrecisionCircle)
MapFilterState(favoritesOnly, showWaypoints, showPrecisionCircle, lastHeard)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue =
MapFilterState(showOnlyFavorites.value, showWaypointsOnMap.value, showPrecisionCircleOnMap.value),
MapFilterState(
showOnlyFavorites.value,
showWaypointsOnMap.value,
showPrecisionCircleOnMap.value,
lastHeardFilter.value,
),
)
}

Wyświetl plik

@ -29,6 +29,7 @@ interface MapPrefs {
var showOnlyFavorites: Boolean
var showWaypointsOnMap: Boolean
var showPrecisionCircleOnMap: Boolean
var lastHeardFilter: Long
}
@Singleton
@ -37,4 +38,5 @@ class MapPrefsImpl @Inject constructor(@MapSharedPreferences prefs: SharedPrefer
override var showOnlyFavorites: Boolean by PrefDelegate(prefs, "show_only_favorites", false)
override var showWaypointsOnMap: Boolean by PrefDelegate(prefs, "show_waypoints", true)
override var showPrecisionCircleOnMap: Boolean by PrefDelegate(prefs, "show_precision_circle", true)
override var lastHeardFilter: Long by PrefDelegate(prefs, "last_heard_filter", 0L)
}

Wyświetl plik

@ -901,4 +901,10 @@
<string name="remotely_administrating">"[Remote] %1$s"</string>
<string name="device_telemetry_enabled">Send Device Telemetry</string>
<string name="device_telemetry_enabled_summary">Enable/Disable the device telemetry module to send metrics to the mesh</string>
<string name="any">Any</string>
<string name="one_hour">1 Hour</string>
<string name="eight_hours">8 Hours</string>
<string name="one_day">24 Hours</string>
<string name="two_days">48 Hours</string>
<string name="last_heard_filter_label">Filter by Last Heard time: %s</string>
</resources>