feat: add nodelist sort options

pull/1066/head
andrekir 2024-05-27 09:56:26 -03:00 zatwierdzone przez Andre K
rodzic f84a75569d
commit 4ceb4c5199
17 zmienionych plików z 368 dodań i 89 usunięć

Wyświetl plik

@ -5,10 +5,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.dao.NodeInfoDao
import com.geeksville.mesh.model.NodeSortOption
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@ -18,20 +21,20 @@ class NodeDBTest {
private lateinit var database: MeshtasticDatabase
private lateinit var nodeInfoDao: NodeInfoDao
private val testNodeNoPosition = NodeInfo(
8,
MeshUser(
private val ourNodeInfo = NodeInfo(
num = 8,
user = MeshUser(
"+16508765308".format(8),
"Kevin MesterNoLoc",
"Kevin Mester",
"KLO",
MeshProtos.HardwareModel.ANDROID_SIM,
false
),
null
position = Position(30.267153, -97.743057, 35, 123), // Austin
)
private val myNodeInfo: MyNodeInfo = MyNodeInfo(
myNodeNum = testNodeNoPosition.num,
myNodeNum = ourNodeInfo.num,
hasGPS = false,
model = null,
firmwareVersion = null,
@ -47,22 +50,29 @@ class NodeDBTest {
)
private val testPositions = arrayOf(
Position(32.776665, -96.796989, 35, 123), // dallas
Position(32.960758, -96.733521, 35, 456), // richardson
Position(32.912901, -96.781776, 35, 789), // north dallas
Position(32.776665, -96.796989, 35, 123), // Dallas
Position(32.960758, -96.733521, 35, 456), // Richardson
Position(32.912901, -96.781776, 35, 789), // North Dallas
Position(29.760427, -95.369804, 35, 123), // Houston
Position(33.748997, -84.387985, 35, 456), // Atlanta
Position(34.052235, -118.243683, 35, 789), // Los Angeles
Position(40.712776, -74.005974, 35, 123), // New York City
Position(41.878113, -87.629799, 35, 456), // Chicago
Position(39.952583, -75.165222, 35, 789), // Philadelphia
)
private val testNodes = listOf(testNodeNoPosition) + testPositions.mapIndexed { index, it ->
private val testNodes = listOf(ourNodeInfo) + testPositions.mapIndexed { index, it ->
NodeInfo(
9 + index,
MeshUser(
num = 9 + index,
user = MeshUser(
"+165087653%02d".format(9 + index),
"Kevin Mester$index",
"KM$index",
MeshProtos.HardwareModel.ANDROID_SIM,
if (index == 2) MeshProtos.HardwareModel.UNSET else MeshProtos.HardwareModel.ANDROID_SIM,
false
),
it
position = it,
lastHeard = 9 + index,
)
}
@ -72,7 +82,7 @@ class NodeDBTest {
database = Room.inMemoryDatabaseBuilder(context, MeshtasticDatabase::class.java).build()
nodeInfoDao = database.nodeInfoDao()
nodeInfoDao.apply{
nodeInfoDao.apply {
putAll(testNodes)
setMyNodeInfo(myNodeInfo)
}
@ -83,22 +93,83 @@ class NodeDBTest {
database.close()
}
/**
* Retrieves a list of nodes based on [sort], [filter] and [includeUnknown] parameters.
* The list excludes [ourNodeInfo] (our NodeInfo) to ensure consistency in the results.
*/
private suspend fun getNodes(
sort: NodeSortOption = NodeSortOption.LAST_HEARD,
filter: String = "",
includeUnknown: Boolean = true,
) = nodeInfoDao.getNodes(
sort = sort.sqlValue,
filter = filter,
includeUnknown = includeUnknown,
unknownHwModel = MeshProtos.HardwareModel.UNSET
).first().filter { it != ourNodeInfo }
@Test // node list size
fun testNodeListSize() = runBlocking {
val nodes = nodeInfoDao.nodeDBbyNum().first()
assertEquals(nodes.size, 4)
assertEquals(10, nodes.size)
}
@Test // nodeDBbyNum() re-orders our node at the top of the list
fun testOurNodeIntoIsFirst() = runBlocking {
fun testOurNodeInfoIsFirst() = runBlocking {
val nodes = nodeInfoDao.nodeDBbyNum().first()
assertEquals(nodes.values.first(), testNodeNoPosition)
assertEquals(ourNodeInfo, nodes.values.first())
}
@Test // getNodeInfo()
fun testGetNodeInfo() = runBlocking {
for (node in nodeInfoDao.getNodes().first()) {
assertEquals(nodeInfoDao.getNodeInfo(node.num), node)
@Test
fun testSortByLastHeard() = runBlocking {
val nodes = getNodes(sort = NodeSortOption.LAST_HEARD)
val sortedNodes = nodes.sortedByDescending { it.lastHeard }
assertEquals(sortedNodes, nodes)
}
@Test
fun testSortByAlpha() = runBlocking {
val nodes = getNodes(sort = NodeSortOption.ALPHABETICAL)
val sortedNodes = nodes.sortedBy { it.user?.longName?.uppercase() }
assertEquals(sortedNodes, nodes)
}
@Test
fun testSortByDistance() = runBlocking {
val nodes = getNodes(sort = NodeSortOption.DISTANCE)
val sortedNodes = nodes.sortedBy { it.distance(ourNodeInfo) }
assertEquals(sortedNodes, nodes)
}
@Test
fun testSortByChannel() = runBlocking {
val nodes = getNodes(sort = NodeSortOption.CHANNEL)
val sortedNodes = nodes.sortedBy { it.channel }
assertEquals(sortedNodes, nodes)
}
@Test
fun testSortByViaMqtt() = runBlocking {
val nodes = getNodes(sort = NodeSortOption.VIA_MQTT)
val sortedNodes = nodes.sortedBy { it.user?.longName?.contains("(MQTT)") == true }
assertEquals(sortedNodes, nodes)
}
@Test
fun testIncludeUnknownIsFalse() = runBlocking {
val nodes = getNodes(includeUnknown = false)
val containsUnsetNode = nodes.any { node ->
node.user?.hwModel == MeshProtos.HardwareModel.UNSET
}
assertFalse(containsUnsetNode)
}
@Test
fun testIncludeUnknownIsTrue() = runBlocking {
val nodes = getNodes(includeUnknown = true)
val containsUnsetNode = nodes.any { node ->
node.user?.hwModel == MeshProtos.HardwareModel.UNSET
}
assertTrue(containsUnsetNode)
}
}

Wyświetl plik

@ -6,6 +6,7 @@ import androidx.room.MapColumn
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Upsert
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MyNodeInfo
import com.geeksville.mesh.NodeInfo
import kotlinx.coroutines.flow.Flow
@ -22,17 +23,54 @@ interface NodeInfoDao {
@Query("DELETE FROM MyNodeInfo")
fun clearMyNodeInfo()
@Query("SELECT * FROM NodeInfo")
fun getNodes(): Flow<List<NodeInfo>>
@Query("SELECT * FROM NodeInfo ORDER BY CASE WHEN num = (SELECT myNodeNum FROM MyNodeInfo LIMIT 1) THEN 0 ELSE 1 END, lastHeard DESC")
fun nodeDBbyNum(): Flow<Map<@MapColumn(columnName = "num") Int, NodeInfo>>
@Query("SELECT * FROM NodeInfo")
fun nodeDBbyID(): Flow<Map<@MapColumn(columnName = "user_id") String, NodeInfo>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(node: NodeInfo)
@Query(
"""
WITH OurNode AS (
SELECT position_latitude, position_longitude
FROM NodeInfo
WHERE num = (SELECT myNodeNum FROM MyNodeInfo LIMIT 1)
)
SELECT * FROM NodeInfo
WHERE (:includeUnknown = 1 OR user_hwModel != :unknownHwModel)
AND (:filter = ''
OR (user_longName LIKE '%' || :filter || '%'
OR user_shortName LIKE '%' || :filter || '%'))
ORDER BY CASE
WHEN num = (SELECT myNodeNum FROM MyNodeInfo LIMIT 1) THEN 0
ELSE 1
END,
CASE
WHEN :sort = 'last_heard' THEN lastHeard * -1
WHEN :sort = 'alpha' THEN UPPER(user_longName)
WHEN :sort = 'distance' THEN
CASE
WHEN position_latitude IS NULL OR position_longitude IS NULL OR
(position_latitude = 0 AND position_longitude = 0) THEN 999999999
ELSE
(position_latitude - (SELECT position_latitude FROM OurNode)) *
(position_latitude - (SELECT position_latitude FROM OurNode)) +
(position_longitude - (SELECT position_longitude FROM OurNode)) *
(position_longitude - (SELECT position_longitude FROM OurNode))
END
WHEN :sort = 'channel' THEN channel
WHEN :sort = 'via_mqtt' THEN user_longName LIKE '%(MQTT)' -- viaMqtt
ELSE 0
END ASC,
lastHeard DESC
"""
)
fun getNodes(
sort: String,
filter: String,
includeUnknown: Boolean,
unknownHwModel: MeshProtos.HardwareModel
): Flow<List<NodeInfo>>
@Upsert
fun upsert(node: NodeInfo)
@ -45,18 +83,4 @@ interface NodeInfoDao {
@Query("DELETE FROM NodeInfo WHERE num=:num")
fun delNode(num: Int)
@Query("SELECT * FROM NodeInfo WHERE num=:num")
fun getNodeInfo(num: Int): NodeInfo?
// @Transaction
// suspend fun updateUser(num: Int, updatedUser: MeshUser) {
// getNodeInfo(num)?.let {
// val updatedNodeInfo = it.copy(user = updatedUser)
// upsert(updatedNodeInfo)
// }
// }
// @Query("Update node_info set position=:position WHERE num=:num")
// fun updatePosition(num: Int, position: MeshProtos.Position)
}

Wyświetl plik

@ -2,6 +2,7 @@ package com.geeksville.mesh.model
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MyNodeInfo
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.database.dao.NodeInfoDao
@ -20,7 +21,6 @@ class NodeDB @Inject constructor(
processLifecycle: Lifecycle,
private val nodeInfoDao: NodeInfoDao,
) {
// hardware info about our local device (can be null)
private val _myNodeInfo = MutableStateFlow<MyNodeInfo?>(null)
val myNodeInfo: StateFlow<MyNodeInfo?> get() = _myNodeInfo
@ -58,8 +58,19 @@ class NodeDB @Inject constructor(
.launchIn(processLifecycle.coroutineScope)
}
fun getNodes(
sort: NodeSortOption = NodeSortOption.LAST_HEARD,
filter: String = "",
includeUnknown: Boolean = true,
) = nodeInfoDao.getNodes(
sort = sort.sqlValue,
filter = filter,
includeUnknown = includeUnknown,
unknownHwModel = MeshProtos.HardwareModel.UNSET
)
fun myNodeInfoFlow(): Flow<MyNodeInfo?> = nodeInfoDao.getMyNodeInfo()
fun nodeInfoFlow(): Flow<List<NodeInfo>> = nodeInfoDao.getNodes()
fun delNode(num: Int) {
nodeInfoDao.delNode(num)
}

Wyświetl plik

@ -0,0 +1,12 @@
package com.geeksville.mesh.model
import androidx.annotation.StringRes
import com.geeksville.mesh.R
enum class NodeSortOption(val sqlValue: String, @StringRes val stringRes: Int) {
LAST_HEARD("last_heard", R.string.node_sort_last_heard),
ALPHABETICAL("alpha", R.string.node_sort_alpha),
DISTANCE("distance", R.string.node_sort_distance),
CHANNEL("channel", R.string.node_sort_channel),
VIA_MQTT("via_mqtt", R.string.node_sort_via_mqtt),
}

Wyświetl plik

@ -32,6 +32,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
@ -39,6 +40,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedWriter
@ -97,6 +99,16 @@ internal fun getChannelList(
}
}
data class NodesUiState(
val sort: NodeSortOption = NodeSortOption.LAST_HEARD,
val filter: String = "",
val includeUnknown: Boolean = false,
) {
companion object {
val Empty = NodesUiState()
}
}
@HiltViewModel
class UIViewModel @Inject constructor(
private val app: Application,
@ -138,18 +150,43 @@ class UIViewModel @Inject constructor(
private val _focusedNode = MutableStateFlow<NodeInfo?>(null)
val focusedNode: StateFlow<NodeInfo?> = _focusedNode
private val _nodeFilterText = MutableStateFlow("")
val nodeFilterText: StateFlow<String> = _nodeFilterText
private val nodeFilterText = MutableStateFlow("")
private val nodeSortOption = MutableStateFlow(NodeSortOption.LAST_HEARD)
private val includeUnknown = MutableStateFlow(false)
val filteredNodes = nodeDB.nodeDBbyNum.combine(_nodeFilterText) { nodes, filterText ->
if (filterText.isBlank()) return@combine nodes
nodes.filter { entry ->
entry.value.user?.longName?.contains(filterText, ignoreCase = true) == true ||
entry.value.user?.shortName?.contains(filterText, ignoreCase = true) == true
}
fun setSortOption(sort: NodeSortOption) {
nodeSortOption.value = sort
}
fun toggleIncludeUnknown() {
includeUnknown.value = !includeUnknown.value
}
val nodeViewState: StateFlow<NodesUiState> = combine(
nodeFilterText,
nodeSortOption,
includeUnknown,
) { filter, sort, includeUnknown ->
NodesUiState(
sort = sort,
filter = filter,
includeUnknown = includeUnknown,
)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(),
initialValue = NodesUiState.Empty,
)
@OptIn(ExperimentalCoroutinesApi::class)
val filteredNodes: StateFlow<List<NodeInfo>> = nodeViewState.flatMapLatest { state ->
nodeDB.getNodes(state.sort, state.filter, state.includeUnknown)
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5_000),
initialValue = emptyList(),
)
// hardware info about our local device (can be null)
val myNodeInfo: StateFlow<MyNodeInfo?> get() = nodeDB.myNodeInfo
val ourNodeInfo: StateFlow<NodeInfo?> get() = nodeDB.ourNodeInfo
@ -596,7 +633,7 @@ class UIViewModel @Inject constructor(
}
fun setNodeFilterText(text: String) {
_nodeFilterText.value = text
nodeFilterText.value = text
}
}

Wyświetl plik

@ -84,7 +84,7 @@ class RadioConfigRepository @Inject constructor(
/**
* Flow representing the [NodeInfo] database.
*/
suspend fun getNodes(): List<NodeInfo>? = nodeDB.nodeInfoFlow().firstOrNull()
suspend fun getNodes(): List<NodeInfo>? = nodeDB.getNodes().firstOrNull()
suspend fun upsert(node: NodeInfo) = nodeDB.upsert(node)
suspend fun installNodeDB(mi: MyNodeInfo, nodes: List<NodeInfo>) {

Wyświetl plik

@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.compose.ElevationInfo
@ -137,7 +138,7 @@ fun NodeInfo(
)
}
val style = if (nodeName == unknownLongName) {
val style = if (thatNodeInfo.user?.hwModel == MeshProtos.HardwareModel.UNSET) {
LocalTextStyle.current.copy(fontStyle = FontStyle.Italic)
} else {
LocalTextStyle.current

Wyświetl plik

@ -0,0 +1,89 @@
package com.geeksville.mesh.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
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.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
import com.geeksville.mesh.model.NodeSortOption
@Composable
internal fun NodeSortButton(
currentSortOption: NodeSortOption,
onSortSelected: (NodeSortOption) -> Unit,
includeUnknown: Boolean,
onToggleIncludeUnknown: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier) {
var expanded by remember { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_twotone_sort_24),
contentDescription = null,
modifier = Modifier.heightIn(max = 48.dp),
tint = MaterialTheme.colors.onSurface
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.background(MaterialTheme.colors.background.copy(alpha = 1f))
) {
NodeSortOption.entries.forEach { sort ->
DropdownMenuItem(
onClick = {
onSortSelected(sort)
expanded = false
},
) {
Text(
text = stringResource(id = sort.stringRes),
fontWeight = if (sort == currentSortOption) FontWeight.Bold else null,
)
}
}
Divider()
DropdownMenuItem(
onClick = {
onToggleIncludeUnknown()
expanded = false
},
) {
Text(
text = stringResource(id = R.string.node_filter_include_unknown),
)
AnimatedVisibility(visible = includeUnknown) {
Icon(
imageVector = Icons.Default.Done,
contentDescription = null,
modifier = Modifier.padding(start = 4.dp),
)
}
}
}
}
}

Wyświetl plik

@ -7,21 +7,20 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.asLiveData
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
@ -262,7 +261,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
binding.nodeFilter.initFilter()
model.filteredNodes.asLiveData().observe(viewLifecycleOwner) { nodeMap ->
nodesAdapter.onNodesChanged(nodeMap.values.toTypedArray())
nodesAdapter.onNodesChanged(nodeMap.toTypedArray())
}
model.localConfig.asLiveData().observe(viewLifecycleOwner) { config ->
@ -343,17 +342,24 @@ class UsersFragment : ScreenFragment("Users"), Logging {
private fun ComposeView.initFilter() {
this.setContent {
val filterText by model.nodeFilterText.collectAsState()
val nodeViewState by model.nodeViewState.collectAsStateWithLifecycle()
AppTheme {
Box(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.shadow(8.dp)
) {
NodeFilterTextField(
filterText = filterText,
onTextChanged = { model.setNodeFilterText(it) }
filterText = nodeViewState.filter,
onTextChanged = model::setNodeFilterText,
modifier = Modifier.weight(1f)
)
NodeSortButton(
currentSortOption = nodeViewState.sort,
onSortSelected = model::setSortOption,
includeUnknown = nodeViewState.includeUnknown,
onToggleIncludeUnknown = model::toggleIncludeUnknown,
)
}
}

Wyświetl plik

@ -13,8 +13,14 @@ import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
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.Modifier
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
@ -26,15 +32,17 @@ import com.geeksville.mesh.ui.theme.AppTheme
@Composable
fun NodeFilterTextField(
filterText : String = "",
onTextChanged : (String) -> Unit
filterText : String,
onTextChanged : (String) -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
var isFocused by remember { mutableStateOf(false) }
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
modifier = modifier
.heightIn(max = 48.dp)
.onFocusEvent { isFocused = it.isFocused }
.background(MaterialTheme.colors.background),
value = filterText,
placeholder = {
@ -44,13 +52,22 @@ fun NodeFilterTextField(
color = MaterialTheme.colors.onBackground.copy(alpha = 0.35F)
)
},
leadingIcon = {
Icon(
Icons.Default.Search,
contentDescription = stringResource(id = R.string.node_filter_placeholder),
)
},
onValueChange = onTextChanged,
trailingIcon = {
if (filterText.isNotEmpty()) {
if (filterText.isNotEmpty() || isFocused) {
Icon(
Icons.Default.Clear,
contentDescription = stringResource(id = R.string.desc_node_filter_clear),
modifier = Modifier.clickable { onTextChanged("") }
modifier = Modifier.clickable {
onTextChanged("")
focusManager.clearFocus()
}
)
}
},

Wyświetl plik

@ -1,4 +1,4 @@
package com.geeksville.mesh.ui.map.components
package com.geeksville.mesh.ui.map
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
@Composable
fun CacheLayout(
internal fun CacheLayout(
cacheEstimate: String,
onExecuteJob: () -> Unit,
onCancelDownload: () -> Unit,

Wyświetl plik

@ -1,4 +1,4 @@
package com.geeksville.mesh.ui.map.components
package com.geeksville.mesh.ui.map
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource
import com.geeksville.mesh.R
@Composable
fun DownloadButton(
internal fun DownloadButton(
enabled: Boolean,
onClick: () -> Unit,
) {

Wyświetl plik

@ -1,4 +1,4 @@
package com.geeksville.mesh.ui.map.components
package com.geeksville.mesh.ui.map
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -42,7 +42,7 @@ import com.geeksville.mesh.util.CustomRecentEmojiProvider
import com.geeksville.mesh.waypoint
@Composable
fun EditWaypointDialog(
internal fun EditWaypointDialog(
waypoint: Waypoint,
onSendClicked: (Waypoint) -> Unit,
onDeleteClicked: (Waypoint) -> Unit,

Wyświetl plik

@ -50,19 +50,14 @@ import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.map.CustomTileSource
import com.geeksville.mesh.model.map.MarkerWithLabel
import com.geeksville.mesh.ui.MessagesFragment
import com.geeksville.mesh.ui.ScreenFragment
import com.geeksville.mesh.ui.map.components.CacheLayout
import com.geeksville.mesh.ui.map.components.DownloadButton
import com.geeksville.mesh.ui.map.components.EditWaypointDialog
import com.geeksville.mesh.ui.components.IconButton
import com.geeksville.mesh.ui.map.components.rememberMapViewWithLifecycle
import com.geeksville.mesh.ui.theme.AppTheme
import com.geeksville.mesh.util.SqlTileWriterExt
import com.geeksville.mesh.util.requiredZoomLevel
import com.geeksville.mesh.util.formatAgo
import com.geeksville.mesh.util.zoomIn
import com.geeksville.mesh.waypoint
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable
@ -90,7 +85,6 @@ import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import java.io.File
import java.text.DateFormat
@AndroidEntryPoint
class MapFragment : ScreenFragment("Map Fragment"), Logging {
@ -104,7 +98,7 @@ class MapFragment : ScreenFragment("Map Fragment"), Logging {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
AppCompatTheme {
AppTheme {
MapView(model)
}
}
@ -192,7 +186,7 @@ fun MapView(
requestPermissionAndToggleLauncher.launch(context.getLocationPermissions())
}
val nodes by model.nodeDB.nodes.collectAsStateWithLifecycle()
val nodes by model.filteredNodes.collectAsStateWithLifecycle(emptyList())
val waypoints by model.waypoints.observeAsState(emptyMap())
var showDownloadButton: Boolean by remember { mutableStateOf(false) }
@ -462,7 +456,7 @@ fun MapView(
}
with(map) {
UpdateMarkers(onNodesChanged(nodes.values), onWaypointChanged(waypoints.values))
UpdateMarkers(onNodesChanged(nodes), onWaypointChanged(waypoints.values))
}
// private fun addWeatherLayer() {
@ -482,7 +476,7 @@ fun MapView(
// }
fun MapView.zoomToNodes() {
val nodeMarkers = onNodesChanged(nodes.values)
val nodeMarkers = onNodesChanged(nodes)
if (nodeMarkers.isNotEmpty()) {
val box = BoundingBox.fromGeoPoints(nodeMarkers.map { it.position })
val center = GeoPoint(box.centerLatitude, box.centerLongitude)

Wyświetl plik

@ -1,4 +1,4 @@
package com.geeksville.mesh.ui.map.components
package com.geeksville.mesh.ui.map
import android.annotation.SuppressLint
import android.content.Context
@ -32,7 +32,7 @@ private fun PowerManager.WakeLock.safeRelease() {
}
@Composable
fun rememberMapViewWithLifecycle(context: Context): MapView {
internal fun rememberMapViewWithLifecycle(context: Context): MapView {
val mapView = remember {
MapView(context).apply {
clipToOutline = true

Wyświetl plik

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z" />
</vector>

Wyświetl plik

@ -16,6 +16,12 @@
<string name="unknown_node_short_name" translatable="false">\???</string>
<string name="node_filter_placeholder">Filter</string>
<string name="desc_node_filter_clear">clear node filter</string>
<string name="node_filter_include_unknown">Include unknown</string>
<string name="node_sort_alpha">A-Z</string>
<string name="node_sort_channel">Channel</string>
<string name="node_sort_distance">Distance</string>
<string name="node_sort_last_heard">Last heard</string>
<string name="node_sort_via_mqtt">via MQTT</string>
<string name="elevation_suffix">ASL</string>