From b90f614794a178e9e41bb35f569b10b853aa809e Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:48:05 -0500 Subject: [PATCH 1/3] feat: Add more details screen for nodes This commit introduces a new "More Details" screen for nodes, accessible via the node list's context menu. The screen displays the node's long name and provides a back button to return to the node list. This feature allows users to view additional information about individual nodes. --- .../java/com/geeksville/mesh/model/UIState.kt | 13 +- .../geeksville/mesh/ui/NodeDetailsFragment.kt | 112 ++++++++++++++++++ .../com/geeksville/mesh/ui/UsersFragment.kt | 9 ++ app/src/main/res/menu/menu_nodes.xml | 4 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/NodeDetailsFragment.kt 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 bec391d9c..01bba26c6 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -194,10 +195,19 @@ class UIViewModel @Inject constructor( nodeDB.getNodes(state.sort, state.filter, state.includeUnknown) }.stateIn( scope = viewModelScope, - started = WhileSubscribed(5_000), + started = WhileSubscribed(STOP_TIMEOUT_MILLIS), initialValue = emptyList(), ) + fun getNodeByNum(nodeNum: Int): StateFlow = nodeList.map { list -> + list.firstOrNull { it.num == nodeNum } + }.stateIn( + scope = viewModelScope, + started = WhileSubscribed(STOP_TIMEOUT_MILLIS), + initialValue = null, + ) + + // hardware info about our local device (can be null) val myNodeInfo: StateFlow get() = nodeDB.myNodeInfo val ourNodeInfo: StateFlow get() = nodeDB.ourNodeInfo @@ -315,6 +325,7 @@ class UIViewModel @Inject constructor( } companion object { + private const val STOP_TIMEOUT_MILLIS = 5_000L fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeDetailsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeDetailsFragment.kt new file mode 100644 index 000000000..651e2e07b --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeDetailsFragment.kt @@ -0,0 +1,112 @@ +package com.geeksville.mesh.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.activityViewModels +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.geeksville.mesh.R +import com.geeksville.mesh.android.Logging +import com.geeksville.mesh.model.UIViewModel +import com.geeksville.mesh.ui.theme.AppTheme +import dagger.hilt.android.AndroidEntryPoint + +internal fun FragmentManager.navigateToNodeDetails(nodeNum: Int? = null) { + val nodeDetailsFragment = NodeDetailsFragment().apply { + arguments = bundleOf("nodeNum" to nodeNum) + } + beginTransaction() + .replace(R.id.mainActivityLayout, nodeDetailsFragment) + .addToBackStack(null) + .commit() +} + +@AndroidEntryPoint +class NodeDetailsFragment : ScreenFragment("NodeDetails"), Logging { + + private val model: UIViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val nodeNum = arguments?.getInt("nodeNum") + + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + AppTheme { + NodeDetailsScreen( + model = model, + nodeNum = nodeNum, + navigateBack = { + parentFragmentManager.popBackStack() + } + ) + } + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun NodeDetailsScreen( + model: UIViewModel = hiltViewModel(), + nodeNum: Int? = null, + navigateBack: () -> Unit, +) { + val ourNodeInfo by model.ourNodeInfo.collectAsStateWithLifecycle() + val detailsNodeInfo by model.getNodeByNum(nodeNum ?: ourNodeInfo?.num ?: 0) + .collectAsStateWithLifecycle() + Scaffold( + topBar = { + TopAppBar( + title = { + Text("Node Details: ${detailsNodeInfo?.user?.shortName}") + }, + navigationIcon = { + IconButton(onClick = navigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Localized description" + ) + } + } + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + modifier = Modifier.padding(8.dp), + text = detailsNodeInfo?.user?.longName.orEmpty(), + ) + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt index c22464801..b0ab4c27b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt @@ -81,6 +81,10 @@ class UsersFragment : ScreenFragment("Users"), Logging { R.id.remote_admin -> { navigateToRadioConfig(node) } + + R.id.more_details -> { + navigateToMoreDetails(node) + } } } } @@ -96,6 +100,11 @@ class UsersFragment : ScreenFragment("Users"), Logging { parentFragmentManager.navigateToRadioConfig(node.num) } + private fun navigateToMoreDetails(node: NodeInfo) { + info("calling MoreDetails --> destNum: ${node.num}") + parentFragmentManager.navigateToNodeDetails(node.num) + } + override fun onCreateView( inflater: LayoutInflater, diff --git a/app/src/main/res/menu/menu_nodes.xml b/app/src/main/res/menu/menu_nodes.xml index 1ae970950..eb70ca473 100644 --- a/app/src/main/res/menu/menu_nodes.xml +++ b/app/src/main/res/menu/menu_nodes.xml @@ -23,6 +23,10 @@ android:id="@+id/remove" android:title="@string/remove" app:showAsAction="withText" /> + Last heard via MQTT + More Details + MSL Channel Name From 761506600253e0e116499b8d0846cc842794e8cb Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:17:38 -0500 Subject: [PATCH 2/3] feat: Sets background and content colors of the app bar. --- .../java/com/geeksville/mesh/ui/NodeDetailsFragment.kt | 10 ++++++++-- app/src/main/res/values/strings.xml | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeDetailsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeDetailsFragment.kt index 651e2e07b..25791f6e4 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NodeDetailsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeDetailsFragment.kt @@ -20,6 +20,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.FragmentManager @@ -84,14 +86,18 @@ fun NodeDetailsScreen( Scaffold( topBar = { TopAppBar( + backgroundColor = colorResource(R.color.toolbarBackground), + contentColor = colorResource(R.color.toolbarText), title = { - Text("Node Details: ${detailsNodeInfo?.user?.shortName}") + Text( + text = "Node Details: ${detailsNodeInfo?.user?.shortName}", + ) }, navigationIcon = { IconButton(onClick = navigateBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Localized description" + contentDescription = stringResource(R.string.navigate_back), ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6fee8b40c..a984324f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -216,4 +216,5 @@ Scanned Channels Current Channels Replace + Navigate Back From 3241a50d77c60afa62779bfd0d98640579585d4d Mon Sep 17 00:00:00 2001 From: James Rich Date: Sat, 3 Aug 2024 08:59:56 -0500 Subject: [PATCH 3/3] Refactor: Use nodeDBbyNum to get node by number This commit refactors the getNodeByNum function to use the in memory nodeDBbyNum map instead of iterating through the nodeList. This improves performance and readability. --- app/src/main/java/com/geeksville/mesh/model/UIState.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 01bba26c6..24540eb87 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -199,8 +199,9 @@ class UIViewModel @Inject constructor( initialValue = emptyList(), ) - fun getNodeByNum(nodeNum: Int): StateFlow = nodeList.map { list -> - list.firstOrNull { it.num == nodeNum } + @OptIn(ExperimentalCoroutinesApi::class) + fun getNodeByNum(nodeNum: Int): StateFlow = nodeDB.nodeDBbyNum.mapLatest { nodes -> + nodes[nodeNum] }.stateIn( scope = viewModelScope, started = WhileSubscribed(STOP_TIMEOUT_MILLIS),