kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat(NodeDetails): add loading state indicator and pager tabs
rodzic
df2847dd6f
commit
b17bdd4fb8
|
@ -1,15 +1,39 @@
|
|||
package com.geeksville.mesh.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
||||
import com.geeksville.mesh.database.MeshLogRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.enums.EnumEntries
|
||||
|
||||
enum class NodeDetailPage(
|
||||
@StringRes val titleResId: Int,
|
||||
@DrawableRes val drawableResId: Int,
|
||||
) {
|
||||
DEVICE(R.string.device_metrics, R.drawable.baseline_charging_station_24),
|
||||
ENVIRONMENT(R.string.environment_metrics, R.drawable.baseline_thermostat_24),
|
||||
}
|
||||
|
||||
data class NodeDetailsState(
|
||||
val pages: EnumEntries<NodeDetailPage> = NodeDetailPage.entries,
|
||||
val isLoading: Boolean = false,
|
||||
val deviceMetrics: List<Telemetry> = emptyList(),
|
||||
val environmentMetrics: List<Telemetry> = emptyList(),
|
||||
) {
|
||||
companion object {
|
||||
val Empty = NodeDetailsState()
|
||||
}
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
class NodeDetailsViewModel @Inject constructor(
|
||||
|
@ -17,11 +41,25 @@ class NodeDetailsViewModel @Inject constructor(
|
|||
private val meshLogRepository: MeshLogRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val isLoading = MutableStateFlow(false)
|
||||
private val _deviceMetrics = MutableStateFlow<List<Telemetry>>(emptyList())
|
||||
val deviceMetrics: StateFlow<List<Telemetry>> = _deviceMetrics
|
||||
|
||||
private val _environmentMetrics = MutableStateFlow<List<Telemetry>>(emptyList())
|
||||
val environmentMetrics: StateFlow<List<Telemetry>> = _environmentMetrics
|
||||
|
||||
val state = combine(
|
||||
isLoading,
|
||||
_deviceMetrics,
|
||||
_environmentMetrics,
|
||||
) { isLoading, device, environment ->
|
||||
NodeDetailsState(
|
||||
isLoading = isLoading,
|
||||
deviceMetrics = device,
|
||||
environmentMetrics = environment,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = WhileSubscribed(5_000),
|
||||
initialValue = NodeDetailsState.Empty,
|
||||
)
|
||||
|
||||
/**
|
||||
* Gets the short name of the node identified by `nodeNum`.
|
||||
|
@ -33,6 +71,7 @@ class NodeDetailsViewModel @Inject constructor(
|
|||
*/
|
||||
fun setSelectedNode(nodeNum: Int) {
|
||||
viewModelScope.launch {
|
||||
isLoading.value = true
|
||||
meshLogRepository.getTelemetryFrom(nodeNum).collect {
|
||||
val deviceList = mutableListOf<Telemetry>()
|
||||
val environmentList = mutableListOf<Telemetry>()
|
||||
|
@ -45,6 +84,7 @@ class NodeDetailsViewModel @Inject constructor(
|
|||
}
|
||||
_deviceMetrics.value = deviceList
|
||||
_environmentMetrics.value = environmentList
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,32 +5,35 @@ 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.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Tab
|
||||
import androidx.compose.material.TabRow
|
||||
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.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
|
@ -38,11 +41,14 @@ 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.NodeDetailPage
|
||||
import com.geeksville.mesh.model.NodeDetailsState
|
||||
import com.geeksville.mesh.model.NodeDetailsViewModel
|
||||
import com.geeksville.mesh.ui.components.DeviceMetricsScreen
|
||||
import com.geeksville.mesh.ui.components.EnvironmentMetricsScreen
|
||||
import com.geeksville.mesh.ui.theme.AppTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal fun FragmentManager.navigateToNodeDetails(nodeNum: Int? = null) {
|
||||
val nodeDetailsFragment = NodeDetailsFragment().apply {
|
||||
|
@ -94,10 +100,8 @@ fun NodeDetailsScreen(
|
|||
nodeName: String?,
|
||||
navigateBack: () -> Unit,
|
||||
) {
|
||||
val deviceMetrics by model.deviceMetrics.collectAsStateWithLifecycle()
|
||||
val environmentMetrics by model.environmentMetrics.collectAsStateWithLifecycle()
|
||||
|
||||
val pagerState = rememberPagerState(pageCount = { 2 })
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val pagerState = rememberPagerState(pageCount = { state.pages.size })
|
||||
|
||||
Scaffold(
|
||||
/*
|
||||
|
@ -111,7 +115,6 @@ fun NodeDetailsScreen(
|
|||
Text(
|
||||
text = "${stringResource(R.string.node_details)}: $nodeName",
|
||||
)
|
||||
HorizontalTabs(pagerState)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigateBack) {
|
||||
|
@ -124,53 +127,74 @@ fun NodeDetailsScreen(
|
|||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
HorizontalPager(state = pagerState) { page ->
|
||||
when (page) {
|
||||
0 -> DeviceMetricsScreen(
|
||||
innerPadding = innerPadding,
|
||||
telemetries = deviceMetrics
|
||||
)
|
||||
1 -> EnvironmentMetricsScreen(
|
||||
innerPadding = innerPadding,
|
||||
telemetries = environmentMetrics
|
||||
NodeDetailsPagerScreen(
|
||||
state = state,
|
||||
pagerState = pagerState,
|
||||
modifier = Modifier.padding(top = innerPadding.calculateTopPadding())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NodeDetailsPagerScreen(
|
||||
state: NodeDetailsState,
|
||||
pagerState: PagerState,
|
||||
modifier: Modifier = Modifier,
|
||||
) = with(state) {
|
||||
Column(modifier) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
TabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
) {
|
||||
pages.forEachIndexed { index, page ->
|
||||
val title = stringResource(id = page.titleResId)
|
||||
Tab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
|
||||
text = { Text(text = title) },
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(id = page.drawableResId),
|
||||
contentDescription = title
|
||||
)
|
||||
},
|
||||
unselectedContentColor = MaterialTheme.colors.secondaryVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) { index ->
|
||||
if (isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
when (pages[index]) {
|
||||
NodeDetailPage.DEVICE -> DeviceMetricsScreen(deviceMetrics)
|
||||
NodeDetailPage.ENVIRONMENT -> EnvironmentMetricsScreen(environmentMetrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun HorizontalTabs(pagerState: PagerState) {
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
repeat(pagerState.pageCount) { iteration ->
|
||||
val color = if (pagerState.currentPage == iteration)
|
||||
colorResource(R.color.toolbarText)
|
||||
else
|
||||
Color.LightGray
|
||||
|
||||
val (imageVector, contentDescription) = if (iteration == 0)
|
||||
Pair(ImageVector.vectorResource(
|
||||
R.drawable.baseline_charging_station_24),
|
||||
stringResource(R.string.device_metrics)
|
||||
)
|
||||
else
|
||||
Pair(
|
||||
ImageVector.vectorResource(R.drawable.baseline_thermostat_24),
|
||||
stringResource(R.string.environment_metrics)
|
||||
)
|
||||
Icon(
|
||||
imageVector,
|
||||
contentDescription,
|
||||
tint = color
|
||||
)
|
||||
}
|
||||
private fun NodeDetailsPreview() {
|
||||
AppTheme {
|
||||
val state = NodeDetailsState.Empty
|
||||
NodeDetailsPagerScreen(
|
||||
state = state,
|
||||
pagerState = rememberPagerState(pageCount = { state.pages.size }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import androidx.compose.foundation.Canvas
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
|
@ -77,7 +76,7 @@ private object ChartConstants {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun DeviceMetricsScreen(innerPadding: PaddingValues, telemetries: List<Telemetry>) {
|
||||
fun DeviceMetricsScreen(telemetries: List<Telemetry>) {
|
||||
Column {
|
||||
DeviceMetricsChart(
|
||||
modifier = Modifier
|
||||
|
@ -87,9 +86,7 @@ fun DeviceMetricsScreen(innerPadding: PaddingValues, telemetries: List<Telemetry
|
|||
)
|
||||
/* Device Metric Cards */
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(telemetries.reversed()) { telemetry -> DeviceMetricsCard(telemetry) }
|
||||
}
|
||||
|
@ -97,7 +94,7 @@ fun DeviceMetricsScreen(innerPadding: PaddingValues, telemetries: List<Telemetry
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun EnvironmentMetricsScreen(innerPadding: PaddingValues, telemetries: List<Telemetry>) {
|
||||
fun EnvironmentMetricsScreen(telemetries: List<Telemetry>) {
|
||||
Column {
|
||||
EnvironmentMetricsChart(
|
||||
modifier = Modifier
|
||||
|
@ -108,9 +105,7 @@ fun EnvironmentMetricsScreen(innerPadding: PaddingValues, telemetries: List<Tele
|
|||
|
||||
/* Environment Metric Cards */
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(telemetries.reversed()) { telemetry -> EnvironmentMetricsCard(telemetry)}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue