mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-13 19:36:09 -04:00
Start migration away from global top app bar (#3132)
This commit is contained in:
@@ -26,6 +26,7 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.map.rememberMapViewWithLifecycle
|
||||
@@ -40,6 +41,7 @@ private const val DEG_D = 1e-7
|
||||
|
||||
@Composable
|
||||
fun NodeMapScreen(
|
||||
navController: NavHostController,
|
||||
@Suppress("UNUSED_PARAMETER") uiViewModel: UIViewModel = hiltViewModel(),
|
||||
viewModel: MetricsViewModel = hiltViewModel(),
|
||||
) {
|
||||
|
||||
@@ -17,20 +17,56 @@
|
||||
|
||||
package com.geeksville.mesh.ui.node
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.map.MapView
|
||||
|
||||
const val DEG_D = 1e-7
|
||||
|
||||
@Composable
|
||||
fun NodeMapScreen(uiViewModel: UIViewModel, metricsViewModel: MetricsViewModel = hiltViewModel()) {
|
||||
fun NodeMapScreen(
|
||||
navController: NavHostController,
|
||||
uiViewModel: UIViewModel,
|
||||
metricsViewModel: MetricsViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by metricsViewModel.state.collectAsState()
|
||||
val positions = state.positionLogs
|
||||
val destNum = state.node?.num
|
||||
MapView(uiViewModel = uiViewModel, focusedNodeNum = destNum, nodeTrack = positions, navigateToNodeDetails = {})
|
||||
val ourNodeInfo by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = state.node?.user?.longName ?: "",
|
||||
ourNode = ourNodeInfo,
|
||||
isConnected = isConnected,
|
||||
showNodeChip = false,
|
||||
canNavigateUp = true,
|
||||
onNavigateUp = navController::navigateUp,
|
||||
actions = {},
|
||||
onAction = {},
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.padding(paddingValues)) {
|
||||
MapView(
|
||||
uiViewModel = uiViewModel,
|
||||
focusedNodeNum = destNum,
|
||||
nodeTrack = positions,
|
||||
navigateToNodeDetails = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,12 @@ fun NavGraphBuilder.connectionsGraph(navController: NavHostController, bluetooth
|
||||
ConnectionsScreen(
|
||||
bluetoothViewModel = bluetoothViewModel,
|
||||
radioConfigViewModel = hiltViewModel(parentEntry),
|
||||
onClickNodeChip = {
|
||||
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
onNavigateToSettings = { navController.navigate(SettingsRoutes.Settings()) },
|
||||
onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
|
||||
onConfigNavigate = { route -> navController.navigate(route) },
|
||||
|
||||
@@ -37,6 +37,12 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, uiViewModel:
|
||||
) {
|
||||
ContactsScreen(
|
||||
uiViewModel,
|
||||
onClickNodeChip = {
|
||||
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
onNavigateToMessages = { navController.navigate(ContactsRoutes.Messages(it)) },
|
||||
onNavigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
|
||||
onNavigateToShare = { navController.navigate(ChannelsRoutes.ChannelsGraph) },
|
||||
|
||||
@@ -17,18 +17,58 @@
|
||||
|
||||
package com.geeksville.mesh.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navDeepLink
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.map.MapView
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
|
||||
fun NavGraphBuilder.mapGraph(navController: NavHostController, uiViewModel: UIViewModel) {
|
||||
composable<MapRoutes.Map>(deepLinks = listOf(navDeepLink<MapRoutes.Map>(basePath = "$DEEP_LINK_BASE_URI/map"))) {
|
||||
MapView(
|
||||
uiViewModel = uiViewModel,
|
||||
navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
|
||||
)
|
||||
val ourNodeInfo by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = stringResource(R.string.map),
|
||||
ourNode = ourNodeInfo,
|
||||
isConnected = isConnected,
|
||||
showNodeChip = ourNodeInfo != null && isConnected,
|
||||
canNavigateUp = false,
|
||||
onNavigateUp = {},
|
||||
actions = {},
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
is NodeMenuAction.MoreDetails -> {
|
||||
navController.navigate(NodesRoutes.NodeDetailGraph(action.node.num)) {
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.padding(paddingValues)) {
|
||||
MapView(
|
||||
uiViewModel = uiViewModel,
|
||||
navigateToNodeDetails = { navController.navigate(NodesRoutes.NodeDetailGraph(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +183,12 @@ private inline fun <reified R : Route> NavGraphBuilder.addNodeDetailScreenCompos
|
||||
navController: NavHostController,
|
||||
uiViewModel: UIViewModel,
|
||||
routeInfo: NodeDetailRoute,
|
||||
crossinline screenContent: @Composable (metricsViewModel: MetricsViewModel, passedUiViewModel: UIViewModel) -> Unit,
|
||||
crossinline screenContent:
|
||||
@Composable (
|
||||
navController: NavHostController,
|
||||
metricsViewModel: MetricsViewModel,
|
||||
passedUiViewModel: UIViewModel,
|
||||
) -> Unit,
|
||||
) {
|
||||
composable<R>(
|
||||
deepLinks =
|
||||
@@ -195,7 +200,7 @@ private inline fun <reified R : Route> NavGraphBuilder.addNodeDetailScreenCompos
|
||||
val parentGraphBackStackEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(NodesRoutes.NodeDetailGraph::class) }
|
||||
val metricsViewModel = hiltViewModel<MetricsViewModel>(parentGraphBackStackEntry)
|
||||
screenContent(metricsViewModel, uiViewModel)
|
||||
screenContent(navController, metricsViewModel, uiViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,60 +208,65 @@ enum class NodeDetailRoute(
|
||||
@StringRes val title: Int,
|
||||
val route: Route,
|
||||
val icon: ImageVector?,
|
||||
val screenComposable: @Composable (metricsViewModel: MetricsViewModel, uiViewModel: UIViewModel) -> Unit,
|
||||
val screenComposable:
|
||||
@Composable (
|
||||
navController: NavHostController,
|
||||
metricsViewModel: MetricsViewModel,
|
||||
uiViewModel: UIViewModel,
|
||||
) -> Unit,
|
||||
) {
|
||||
DEVICE(
|
||||
R.string.device,
|
||||
NodeDetailRoutes.DeviceMetrics,
|
||||
Icons.Default.Router,
|
||||
{ metricsVM, _ -> DeviceMetricsScreen(metricsVM) },
|
||||
{ _, metricsVM, _ -> DeviceMetricsScreen(metricsVM) },
|
||||
),
|
||||
NODE_MAP(
|
||||
R.string.node_map,
|
||||
NodeDetailRoutes.NodeMap,
|
||||
Icons.Default.LocationOn,
|
||||
{ metricsVM, uiVM -> NodeMapScreen(uiVM, metricsVM) },
|
||||
{ navController, metricsVM, uiVM -> NodeMapScreen(navController, uiVM, metricsVM) },
|
||||
),
|
||||
POSITION_LOG(
|
||||
R.string.position_log,
|
||||
NodeDetailRoutes.PositionLog,
|
||||
Icons.Default.LocationOn,
|
||||
{ metricsVM, _ -> PositionLogScreen(metricsVM) },
|
||||
{ _, metricsVM, _ -> PositionLogScreen(metricsVM) },
|
||||
),
|
||||
ENVIRONMENT(
|
||||
R.string.environment,
|
||||
NodeDetailRoutes.EnvironmentMetrics,
|
||||
Icons.Default.LightMode,
|
||||
{ metricsVM, _ -> EnvironmentMetricsScreen(metricsVM) },
|
||||
{ _, metricsVM, _ -> EnvironmentMetricsScreen(metricsVM) },
|
||||
),
|
||||
SIGNAL(
|
||||
R.string.signal,
|
||||
NodeDetailRoutes.SignalMetrics,
|
||||
Icons.Default.CellTower,
|
||||
{ metricsVM, _ -> SignalMetricsScreen(metricsVM) },
|
||||
{ _, metricsVM, _ -> SignalMetricsScreen(metricsVM) },
|
||||
),
|
||||
TRACEROUTE(
|
||||
R.string.traceroute,
|
||||
NodeDetailRoutes.TracerouteLog,
|
||||
Icons.Default.PermScanWifi,
|
||||
{ metricsVM, _ -> TracerouteLogScreen(viewModel = metricsVM) },
|
||||
{ _, metricsVM, _ -> TracerouteLogScreen(viewModel = metricsVM) },
|
||||
),
|
||||
POWER(
|
||||
R.string.power,
|
||||
NodeDetailRoutes.PowerMetrics,
|
||||
Icons.Default.Power,
|
||||
{ metricsVM, _ -> PowerMetricsScreen(metricsVM) },
|
||||
{ _, metricsVM, _ -> PowerMetricsScreen(metricsVM) },
|
||||
),
|
||||
HOST(
|
||||
R.string.host,
|
||||
NodeDetailRoutes.HostMetricsLog,
|
||||
Icons.Default.Memory,
|
||||
{ metricsVM, _ -> HostMetricsLogScreen(metricsVM) },
|
||||
{ _, metricsVM, _ -> HostMetricsLogScreen(metricsVM) },
|
||||
),
|
||||
PAX(
|
||||
R.string.pax,
|
||||
NodeDetailRoutes.PaxMetrics,
|
||||
Icons.Default.People,
|
||||
{ metricsVM, _ -> PaxMetricsScreen(metricsVM) },
|
||||
{ _, metricsVM, _ -> PaxMetricsScreen(metricsVM) },
|
||||
),
|
||||
}
|
||||
|
||||
@@ -97,7 +97,16 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController, uiViewModel:
|
||||
) { backStackEntry ->
|
||||
val parentEntry =
|
||||
remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) }
|
||||
SettingsScreen(uiViewModel = uiViewModel, viewModel = hiltViewModel(parentEntry)) {
|
||||
SettingsScreen(
|
||||
uiViewModel = uiViewModel,
|
||||
viewModel = hiltViewModel(parentEntry),
|
||||
onClickNodeChip = {
|
||||
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
) {
|
||||
navController.navigate(it) { popUpTo(SettingsRoutes.Settings()) { inclusive = false } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.geeksville.mesh.ui
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
@@ -87,6 +88,7 @@ import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.navigation.ConnectionsRoutes
|
||||
import com.geeksville.mesh.navigation.ContactsRoutes
|
||||
import com.geeksville.mesh.navigation.MapRoutes
|
||||
import com.geeksville.mesh.navigation.NodeDetailRoutes
|
||||
import com.geeksville.mesh.navigation.NodesRoutes
|
||||
import com.geeksville.mesh.navigation.Route
|
||||
import com.geeksville.mesh.navigation.SettingsRoutes
|
||||
@@ -135,7 +137,6 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector,
|
||||
NodesRoutes.Nodes::class,
|
||||
MapRoutes.Map::class,
|
||||
ConnectionsRoutes.Connections::class,
|
||||
SettingsRoutes.Settings::class,
|
||||
)
|
||||
.any { this.hasRoute(it) }
|
||||
|
||||
@@ -349,26 +350,42 @@ fun MainScreen(
|
||||
if (sharedContact != null) {
|
||||
SharedContactDialog(contact = sharedContact, onDismiss = { sharedContact = null })
|
||||
}
|
||||
MainAppBar(
|
||||
viewModel = uIViewModel,
|
||||
navController = navController,
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
is NodeMenuAction.MoreDetails -> {
|
||||
navController.navigate(
|
||||
NodesRoutes.NodeDetailGraph(action.node.num),
|
||||
{
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is NodeMenuAction.Share -> sharedContact = action.node
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
)
|
||||
fun NavDestination.hasGlobalAppBar(): Boolean =
|
||||
// List of screens to exclude from having the global app bar
|
||||
listOf(
|
||||
ConnectionsRoutes.Connections::class,
|
||||
ContactsRoutes.Contacts::class,
|
||||
MapRoutes.Map::class,
|
||||
NodeDetailRoutes.NodeMap::class,
|
||||
NodesRoutes.Nodes::class,
|
||||
NodesRoutes.NodeDetail::class,
|
||||
SettingsRoutes.Settings::class,
|
||||
)
|
||||
.none { this.hasRoute(it) }
|
||||
|
||||
AnimatedVisibility(visible = currentDestination?.hasGlobalAppBar() ?: true) {
|
||||
MainAppBar(
|
||||
viewModel = uIViewModel,
|
||||
navController = navController,
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
is NodeMenuAction.MoreDetails -> {
|
||||
navController.navigate(
|
||||
NodesRoutes.NodeDetailGraph(action.node.num),
|
||||
{
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is NodeMenuAction.Share -> sharedContact = action.node
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
|
||||
@@ -75,8 +75,6 @@ fun MainAppBar(
|
||||
}
|
||||
|
||||
val longTitle by viewModel.title.collectAsStateWithLifecycle("")
|
||||
val onlineNodeCount by viewModel.onlineNodeCount.collectAsStateWithLifecycle(0)
|
||||
val totalNodeCount by viewModel.totalNodeCount.collectAsStateWithLifecycle(0)
|
||||
val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val isConnected by viewModel.isConnectedStateFlow.collectAsStateWithLifecycle(false)
|
||||
|
||||
@@ -95,17 +93,10 @@ fun MainAppBar(
|
||||
else -> stringResource(id = R.string.app_name)
|
||||
}
|
||||
|
||||
val subtitle =
|
||||
if (currentDestination?.hasRoute<NodesRoutes.Nodes>() == true) {
|
||||
stringResource(R.string.node_count_template, onlineNodeCount, totalNodeCount)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
MainAppBar(
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
subtitle = null,
|
||||
canNavigateUp = navController.previousBackStackEntry != null && currentDestination?.isTopLevel() == false,
|
||||
ourNode = ourNode,
|
||||
isConnected = isConnected,
|
||||
@@ -125,7 +116,7 @@ fun MainAppBar(
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun MainAppBar(
|
||||
fun MainAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
|
||||
@@ -38,6 +38,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Language
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -69,12 +70,14 @@ import com.geeksville.mesh.navigation.Route
|
||||
import com.geeksville.mesh.navigation.SettingsRoutes
|
||||
import com.geeksville.mesh.navigation.getNavRouteFrom
|
||||
import com.geeksville.mesh.service.ConnectionState
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.common.components.TitledCard
|
||||
import com.geeksville.mesh.ui.connections.components.BLEDevices
|
||||
import com.geeksville.mesh.ui.connections.components.ConnectionsSegmentedBar
|
||||
import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedCard
|
||||
import com.geeksville.mesh.ui.connections.components.CurrentlyConnectedInfo
|
||||
import com.geeksville.mesh.ui.connections.components.NetworkDevices
|
||||
import com.geeksville.mesh.ui.connections.components.UsbDevices
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItem
|
||||
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
|
||||
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
|
||||
@@ -101,6 +104,7 @@ fun ConnectionsScreen(
|
||||
scanModel: BTScanModel = hiltViewModel(),
|
||||
bluetoothViewModel: BluetoothViewModel = hiltViewModel(),
|
||||
radioConfigViewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
onClickNodeChip: (Int) -> Unit,
|
||||
onNavigateToSettings: () -> Unit,
|
||||
onNavigateToNodeDetails: (Int) -> Unit,
|
||||
onConfigNavigate: (Route) -> Unit,
|
||||
@@ -114,6 +118,7 @@ fun ConnectionsScreen(
|
||||
val scanning by scanModel.spinner.collectAsStateWithLifecycle(false)
|
||||
val context = LocalContext.current
|
||||
val info by connectionsViewModel.myNodeInfo.collectAsStateWithLifecycle()
|
||||
val ourNode by connectionsViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle()
|
||||
val bluetoothEnabled by bluetoothViewModel.enabled.collectAsStateWithLifecycle(false)
|
||||
val regionUnset = config.lora.region == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET
|
||||
@@ -179,171 +184,191 @@ fun ConnectionsScreen(
|
||||
SharedContactDialog(contact = showSharedContact, onDismiss = { showSharedContact = null })
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier.fillMaxSize().weight(1f)) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(scrollState).height(IntrinsicSize.Max).padding(16.dp),
|
||||
) {
|
||||
val ourNode by connectionsViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = connectionState.isConnected(),
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = stringResource(R.string.connections),
|
||||
ourNode = ourNode,
|
||||
isConnected = connectionState.isConnected(),
|
||||
showNodeChip = ourNode != null && connectionState.isConnected(),
|
||||
canNavigateUp = false,
|
||||
onNavigateUp = {},
|
||||
actions = {},
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier.fillMaxSize().weight(1f)) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.height(IntrinsicSize.Max)
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
ourNode?.let { node ->
|
||||
Text(
|
||||
stringResource(R.string.connected_device),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
CurrentlyConnectedCard(
|
||||
node = node,
|
||||
onNavigateToNodeDetails = onNavigateToNodeDetails,
|
||||
onSetShowSharedContact = { showSharedContact = it },
|
||||
onNavigateToSettings = onNavigateToSettings,
|
||||
onClickDisconnect = { scanModel.disconnect() },
|
||||
)
|
||||
}
|
||||
|
||||
if (regionUnset && selectedDevice != "m") {
|
||||
TitledCard(title = null) {
|
||||
SettingsItem(
|
||||
leadingIcon = Icons.Rounded.Language,
|
||||
text = stringResource(id = R.string.set_your_region),
|
||||
) {
|
||||
isWaiting = true
|
||||
radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA)
|
||||
AnimatedVisibility(
|
||||
visible = connectionState.isConnected(),
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
ourNode?.let { node ->
|
||||
TitledCard(title = stringResource(R.string.connected_device)) {
|
||||
CurrentlyConnectedInfo(
|
||||
node = node,
|
||||
onNavigateToNodeDetails = onNavigateToNodeDetails,
|
||||
onSetShowSharedContact = { showSharedContact = it },
|
||||
onNavigateToSettings = onNavigateToSettings,
|
||||
onClickDisconnect = { scanModel.disconnect() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectedDeviceType by remember { mutableStateOf(DeviceType.BLE) }
|
||||
LaunchedEffect(selectedDevice) {
|
||||
DeviceType.fromAddress(selectedDevice)?.let { type -> selectedDeviceType = type }
|
||||
}
|
||||
|
||||
ConnectionsSegmentedBar(modifier = Modifier.fillMaxWidth()) { selectedDeviceType = it }
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
when (selectedDeviceType) {
|
||||
DeviceType.BLE -> {
|
||||
BLEDevices(
|
||||
connectionState = connectionState,
|
||||
btDevices = bleDevices,
|
||||
selectedDevice = selectedDevice,
|
||||
scanModel = scanModel,
|
||||
bluetoothEnabled = bluetoothEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
DeviceType.TCP -> {
|
||||
NetworkDevices(
|
||||
connectionState = connectionState,
|
||||
discoveredNetworkDevices = discoveredTcpDevices,
|
||||
recentNetworkDevices = recentTcpDevices,
|
||||
selectedDevice = selectedDevice,
|
||||
scanModel = scanModel,
|
||||
)
|
||||
}
|
||||
|
||||
DeviceType.USB -> {
|
||||
UsbDevices(
|
||||
connectionState = connectionState,
|
||||
usbDevices = usbDevices,
|
||||
selectedDevice = selectedDevice,
|
||||
scanModel = scanModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Warning Not Paired
|
||||
val hasShownNotPairedWarning by
|
||||
connectionsViewModel.hasShownNotPairedWarning.collectAsStateWithLifecycle()
|
||||
val showWarningNotPaired =
|
||||
!connectionState.isConnected() &&
|
||||
!hasShownNotPairedWarning &&
|
||||
bleDevices.none { it is DeviceListEntry.Ble && it.bonded }
|
||||
if (showWarningNotPaired) {
|
||||
Text(
|
||||
text = stringResource(R.string.warning_not_paired),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
LaunchedEffect(Unit) { connectionsViewModel.suppressNoPairedWarning() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose Device Scan Dialog
|
||||
if (showScanDialog) {
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
showScanDialog = false
|
||||
scanModel.clearScanResults()
|
||||
},
|
||||
) {
|
||||
Surface(shape = MaterialTheme.shapes.medium) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Select a Bluetooth device",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
)
|
||||
Column(modifier = Modifier.selectableGroup()) {
|
||||
scanResults.values.forEach { device ->
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = false, // No pre-selection in this dialog
|
||||
onClick = {
|
||||
scanModel.onSelected(device)
|
||||
scanModel.clearScanResults()
|
||||
showScanDialog = false
|
||||
},
|
||||
)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
if (regionUnset && selectedDevice != "m") {
|
||||
TitledCard(title = null) {
|
||||
SettingsItem(
|
||||
leadingIcon = Icons.Rounded.Language,
|
||||
text = stringResource(id = R.string.set_your_region),
|
||||
) {
|
||||
Text(text = device.name)
|
||||
isWaiting = true
|
||||
radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectedDeviceType by remember { mutableStateOf(DeviceType.BLE) }
|
||||
LaunchedEffect(Unit) { DeviceType.fromAddress(selectedDevice)?.let { selectedDeviceType = it } }
|
||||
|
||||
ConnectionsSegmentedBar(
|
||||
selectedDeviceType = selectedDeviceType,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
selectedDeviceType = it
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
when (selectedDeviceType) {
|
||||
DeviceType.BLE -> {
|
||||
BLEDevices(
|
||||
connectionState = connectionState,
|
||||
btDevices = bleDevices,
|
||||
selectedDevice = selectedDevice,
|
||||
scanModel = scanModel,
|
||||
bluetoothEnabled = bluetoothEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
DeviceType.TCP -> {
|
||||
NetworkDevices(
|
||||
connectionState = connectionState,
|
||||
discoveredNetworkDevices = discoveredTcpDevices,
|
||||
recentNetworkDevices = recentTcpDevices,
|
||||
selectedDevice = selectedDevice,
|
||||
scanModel = scanModel,
|
||||
)
|
||||
}
|
||||
|
||||
DeviceType.USB -> {
|
||||
UsbDevices(
|
||||
connectionState = connectionState,
|
||||
usbDevices = usbDevices,
|
||||
selectedDevice = selectedDevice,
|
||||
scanModel = scanModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Warning Not Paired
|
||||
val hasShownNotPairedWarning by
|
||||
connectionsViewModel.hasShownNotPairedWarning.collectAsStateWithLifecycle()
|
||||
val showWarningNotPaired =
|
||||
!connectionState.isConnected() &&
|
||||
!hasShownNotPairedWarning &&
|
||||
bleDevices.none { it is DeviceListEntry.Ble && it.bonded }
|
||||
if (showWarningNotPaired) {
|
||||
Text(
|
||||
text = stringResource(R.string.warning_not_paired),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(
|
||||
onClick = {
|
||||
scanModel.clearScanResults()
|
||||
showScanDialog = false
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
|
||||
LaunchedEffect(Unit) { connectionsViewModel.suppressNoPairedWarning() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose Device Scan Dialog
|
||||
if (showScanDialog) {
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
showScanDialog = false
|
||||
scanModel.clearScanResults()
|
||||
},
|
||||
) {
|
||||
Surface(shape = MaterialTheme.shapes.medium) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Select a Bluetooth device",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
)
|
||||
Column(modifier = Modifier.selectableGroup()) {
|
||||
scanResults.values.forEach { device ->
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = false, // No pre-selection in this dialog
|
||||
onClick = {
|
||||
scanModel.onSelected(device)
|
||||
scanModel.clearScanResults()
|
||||
showScanDialog = false
|
||||
},
|
||||
)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = device.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(
|
||||
onClick = {
|
||||
scanModel.clearScanResults()
|
||||
showScanDialog = false
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
|
||||
Text(
|
||||
text = scanStatusText.orEmpty(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontSize = 10.sp,
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
|
||||
Text(
|
||||
text = scanStatusText.orEmpty(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontSize = 10.sp,
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,6 @@ import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
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
|
||||
@@ -43,19 +39,18 @@ import com.geeksville.mesh.ui.connections.DeviceType
|
||||
|
||||
@Suppress("LambdaParameterEventTrailing")
|
||||
@Composable
|
||||
fun ConnectionsSegmentedBar(modifier: Modifier = Modifier, onClickDeviceType: (DeviceType) -> Unit) {
|
||||
var selectedItem by remember { mutableStateOf(Item.BLUETOOTH) }
|
||||
|
||||
fun ConnectionsSegmentedBar(
|
||||
selectedDeviceType: DeviceType,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickDeviceType: (DeviceType) -> Unit,
|
||||
) {
|
||||
SingleChoiceSegmentedButtonRow(modifier = modifier) {
|
||||
Item.entries.forEachIndexed { index, item ->
|
||||
val text = stringResource(item.textRes)
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(index, Item.entries.size),
|
||||
onClick = {
|
||||
selectedItem = item
|
||||
onClickDeviceType(item.deviceType)
|
||||
},
|
||||
selected = item == selectedItem,
|
||||
onClick = { onClickDeviceType(item.deviceType) },
|
||||
selected = item.deviceType == selectedDeviceType,
|
||||
icon = { Icon(imageVector = item.imageVector, contentDescription = text) },
|
||||
label = { Text(text = text, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||
)
|
||||
@@ -72,5 +67,5 @@ private enum class Item(val imageVector: ImageVector, @StringRes val textRes: In
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun ConnectionsSegmentedBarPreview() {
|
||||
AppTheme { ConnectionsSegmentedBar {} }
|
||||
AppTheme { ConnectionsSegmentedBar(selectedDeviceType = DeviceType.BLE) {} }
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -53,71 +52,69 @@ import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
|
||||
@Composable
|
||||
fun CurrentlyConnectedCard(
|
||||
fun CurrentlyConnectedInfo(
|
||||
node: Node,
|
||||
modifier: Modifier = Modifier,
|
||||
onNavigateToNodeDetails: (Int) -> Unit,
|
||||
onSetShowSharedContact: (Node) -> Unit,
|
||||
onNavigateToSettings: () -> Unit,
|
||||
onClickDisconnect: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(modifier = modifier) {
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
NodeChip(
|
||||
node = node,
|
||||
isThisNode = true,
|
||||
isConnected = true,
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
is NodeMenuAction.MoreDetails -> onNavigateToNodeDetails(node.num)
|
||||
|
||||
is NodeMenuAction.Share -> onSetShowSharedContact(node)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
MaterialBatteryInfo(level = node.batteryLevel)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f, fill = true)) {
|
||||
Text(text = node.user.longName, style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
node.metadata?.firmwareVersion?.let { firmwareVersion ->
|
||||
Text(
|
||||
text = stringResource(R.string.firmware_version, firmwareVersion),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(enabled = true, onClick = onNavigateToSettings) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = stringResource(id = R.string.radio_configuration),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
shape = RectangleShape,
|
||||
modifier = Modifier.fillMaxWidth().height(40.dp),
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.StatusRed,
|
||||
contentColor = Color.White,
|
||||
),
|
||||
onClick = onClickDisconnect,
|
||||
Column(modifier = modifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(stringResource(R.string.disconnect))
|
||||
NodeChip(
|
||||
node = node,
|
||||
isThisNode = true,
|
||||
isConnected = true,
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
is NodeMenuAction.MoreDetails -> onNavigateToNodeDetails(node.num)
|
||||
|
||||
is NodeMenuAction.Share -> onSetShowSharedContact(node)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
MaterialBatteryInfo(level = node.batteryLevel)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f, fill = true)) {
|
||||
Text(text = node.user.longName, style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
node.metadata?.firmwareVersion?.let { firmwareVersion ->
|
||||
Text(
|
||||
text = stringResource(R.string.firmware_version, firmwareVersion),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(enabled = true, onClick = onNavigateToSettings) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = stringResource(id = R.string.radio_configuration),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
shape = RectangleShape,
|
||||
modifier = Modifier.fillMaxWidth().height(40.dp),
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.StatusRed,
|
||||
contentColor = Color.White,
|
||||
),
|
||||
onClick = onClickDisconnect,
|
||||
) {
|
||||
Text(stringResource(R.string.disconnect))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,9 +122,9 @@ fun CurrentlyConnectedCard(
|
||||
@Suppress("MagicNumber")
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun CurrentlyConnectedCardPreview() {
|
||||
private fun CurrentlyConnectedInfoPreview() {
|
||||
AppTheme {
|
||||
CurrentlyConnectedCard(
|
||||
CurrentlyConnectedInfo(
|
||||
node =
|
||||
Node(
|
||||
num = 13444,
|
||||
@@ -57,7 +57,7 @@ private fun UsbDevices(
|
||||
EmptyStateContent(imageVector = Icons.Rounded.UsbOff, text = stringResource(R.string.no_usb_devices))
|
||||
|
||||
else ->
|
||||
TitledCard(title = "") {
|
||||
TitledCard(title = null) {
|
||||
usbDevices.forEach { device ->
|
||||
DeviceListItem(
|
||||
connected =
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package com.geeksville.mesh.ui.contact
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -66,6 +65,8 @@ import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.model.Contact
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@@ -73,11 +74,13 @@ import java.util.concurrent.TimeUnit
|
||||
@Composable
|
||||
fun ContactsScreen(
|
||||
uiViewModel: UIViewModel = hiltViewModel(),
|
||||
onClickNodeChip: (Int) -> Unit = {},
|
||||
onNavigateToMessages: (String) -> Unit = {},
|
||||
onNavigateToNodeDetails: (Int) -> Unit = {},
|
||||
onNavigateToShare: () -> Unit,
|
||||
) {
|
||||
val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle()
|
||||
val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
var showMuteDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -139,6 +142,32 @@ fun ContactsScreen(
|
||||
}
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = stringResource(R.string.conversations),
|
||||
ourNode = ourNode,
|
||||
isConnected = isConnected,
|
||||
showNodeChip = ourNode != null && isConnected,
|
||||
canNavigateUp = false,
|
||||
onNavigateUp = {},
|
||||
actions = {},
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.animateFloatingActionButton(visible = isConnected, alignment = Alignment.BottomEnd),
|
||||
onClick = onNavigateToShare,
|
||||
) {
|
||||
Icon(Icons.Rounded.QrCode2, contentDescription = null)
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
if (isSelectionModeActive) {
|
||||
// Display selection toolbar when in selection mode
|
||||
SelectionToolbar(
|
||||
@@ -153,26 +182,17 @@ fun ContactsScreen(
|
||||
isAllMuted = isAllMuted, // Pass the derived state
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.animateFloatingActionButton(visible = isConnected, alignment = Alignment.BottomEnd),
|
||||
onClick = onNavigateToShare,
|
||||
) {
|
||||
Icon(Icons.Rounded.QrCode2, contentDescription = null)
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
val channels by uiViewModel.channels.collectAsStateWithLifecycle()
|
||||
ContactListView(
|
||||
contacts = contacts,
|
||||
selectedList = selectedContactKeys,
|
||||
onClick = onContactClick,
|
||||
onLongClick = onContactLongClick,
|
||||
contentPadding = paddingValues,
|
||||
channels = channels,
|
||||
onNodeChipClick = onNodeChipClick,
|
||||
)
|
||||
|
||||
val channels by uiViewModel.channels.collectAsStateWithLifecycle()
|
||||
ContactListView(
|
||||
contacts = contacts,
|
||||
selectedList = selectedContactKeys,
|
||||
onClick = onContactClick,
|
||||
onLongClick = onContactLongClick,
|
||||
channels = channels,
|
||||
onNodeChipClick = onNodeChipClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
DeleteConfirmationDialog(
|
||||
showDialog = showDeleteDialog,
|
||||
@@ -351,12 +371,11 @@ fun ContactListView(
|
||||
selectedList: List<String>,
|
||||
onClick: (Contact) -> Unit,
|
||||
onLongClick: (Contact) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
channels: AppOnlyProtos.ChannelSet? = null,
|
||||
onNodeChipClick: (Contact) -> Unit,
|
||||
) {
|
||||
val haptics = LocalHapticFeedback.current
|
||||
LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = contentPadding) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(contacts, key = { it.contactKey }) { contact ->
|
||||
val selected by remember { derivedStateOf { selectedList.contains(contact.contactKey) } }
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -138,6 +139,7 @@ import com.geeksville.mesh.navigation.NodeDetailRoutes
|
||||
import com.geeksville.mesh.navigation.Route
|
||||
import com.geeksville.mesh.navigation.SettingsRoutes
|
||||
import com.geeksville.mesh.service.ServiceAction
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.common.components.TitledCard
|
||||
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
@@ -176,12 +178,13 @@ private data class DrawableMetricInfo(
|
||||
val rotateIcon: Float = 0f,
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun NodeDetailScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: MetricsViewModel = hiltViewModel(),
|
||||
uiViewModel: UIViewModel = hiltViewModel(),
|
||||
navigateToMessages: (String) -> Unit,
|
||||
navigateToMessages: (String) -> Unit = {},
|
||||
onNavigate: (Route) -> Unit = {},
|
||||
onNavigateUp: () -> Unit = {},
|
||||
) {
|
||||
@@ -189,6 +192,7 @@ fun NodeDetailScreen(
|
||||
val environmentState by viewModel.environmentState.collectAsStateWithLifecycle()
|
||||
val lastTracerouteTime by uiViewModel.lastTraceRouteTime.collectAsStateWithLifecycle()
|
||||
val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle(false)
|
||||
|
||||
val availableLogs by
|
||||
remember(state, environmentState) {
|
||||
@@ -210,29 +214,49 @@ fun NodeDetailScreen(
|
||||
}
|
||||
|
||||
val node = state.node
|
||||
if (node != null) {
|
||||
NodeDetailContent(
|
||||
node = node,
|
||||
ourNode = ourNode,
|
||||
metricsState = state,
|
||||
lastTracerouteTime = lastTracerouteTime,
|
||||
availableLogs = availableLogs,
|
||||
uiViewModel = uiViewModel,
|
||||
onAction = { action ->
|
||||
handleNodeAction(
|
||||
action = action,
|
||||
uiViewModel = uiViewModel,
|
||||
node = node,
|
||||
navigateToMessages = navigateToMessages,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onNavigate = onNavigate,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() }
|
||||
|
||||
@Suppress("ModifierNotUsedAtRoot")
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = node?.user?.longName ?: "",
|
||||
ourNode = ourNode,
|
||||
isConnected = isConnected,
|
||||
showNodeChip = false,
|
||||
canNavigateUp = true,
|
||||
onNavigateUp = onNavigateUp,
|
||||
actions = {},
|
||||
onAction = {},
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
if (node != null) {
|
||||
@Suppress("ViewModelForwarding")
|
||||
NodeDetailContent(
|
||||
node = node,
|
||||
ourNode = ourNode,
|
||||
metricsState = state,
|
||||
lastTracerouteTime = lastTracerouteTime,
|
||||
availableLogs = availableLogs,
|
||||
uiViewModel = uiViewModel,
|
||||
onAction = { action ->
|
||||
handleNodeAction(
|
||||
action = action,
|
||||
uiViewModel = uiViewModel,
|
||||
node = node,
|
||||
navigateToMessages = navigateToMessages,
|
||||
onNavigateUp = onNavigateUp,
|
||||
onNavigate = onNavigate,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
},
|
||||
modifier = modifier.padding(paddingValues),
|
||||
)
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(paddingValues), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,14 +42,17 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.service.ConnectionState
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.common.components.rememberTimeTickWithLifecycle
|
||||
import com.geeksville.mesh.ui.node.components.NodeFilterTextField
|
||||
import com.geeksville.mesh.ui.node.components.NodeItem
|
||||
@@ -70,6 +73,8 @@ fun NodeScreen(
|
||||
|
||||
val nodes by model.nodeList.collectAsStateWithLifecycle()
|
||||
val ourNode by model.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val onlineNodeCount by model.onlineNodeCount.collectAsStateWithLifecycle(0)
|
||||
val totalNodeCount by model.totalNodeCount.collectAsStateWithLifecycle(0)
|
||||
val unfilteredNodes by model.unfilteredNodeList.collectAsStateWithLifecycle()
|
||||
val ignoredNodeCount = unfilteredNodes.count { it.isIgnored }
|
||||
|
||||
@@ -85,6 +90,19 @@ fun NodeScreen(
|
||||
|
||||
val isScrollInProgress by remember { derivedStateOf { listState.isScrollInProgress } }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = stringResource(R.string.nodes),
|
||||
subtitle = stringResource(R.string.node_count_template, onlineNodeCount, totalNodeCount),
|
||||
ourNode = ourNode,
|
||||
isConnected = connectionState.isConnected(),
|
||||
showNodeChip = false,
|
||||
canNavigateUp = false,
|
||||
onNavigateUp = {},
|
||||
actions = {},
|
||||
onAction = {},
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
val firmwareVersion = DeviceVersion(ourNode?.metadata?.firmwareVersion ?: "0.0.0")
|
||||
val shareCapable = firmwareVersion.supportsQrCodeSharing()
|
||||
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.material.icons.rounded.LocationOn
|
||||
import androidx.compose.material.icons.rounded.Memory
|
||||
import androidx.compose.material.icons.rounded.Output
|
||||
import androidx.compose.material.icons.rounded.WavingHand
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -59,8 +60,10 @@ import com.geeksville.mesh.android.gpsDisabled
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.navigation.Route
|
||||
import com.geeksville.mesh.navigation.getNavRouteFrom
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.common.components.TitledCard
|
||||
import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItem
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
|
||||
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
|
||||
@@ -83,12 +86,13 @@ import kotlin.time.Duration.Companion.seconds
|
||||
fun SettingsScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
uiViewModel: UIViewModel = hiltViewModel(),
|
||||
onClickNodeChip: (Int) -> Unit = {},
|
||||
onNavigate: (Route) -> Unit = {},
|
||||
) {
|
||||
uiViewModel.setTitle(stringResource(R.string.bottom_nav_settings))
|
||||
|
||||
val excludedModulesUnlocked by uiViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
|
||||
val localConfig by uiViewModel.localConfig.collectAsStateWithLifecycle()
|
||||
val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle(false)
|
||||
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
var isWaiting by remember { mutableStateOf(false) }
|
||||
@@ -162,163 +166,183 @@ fun SettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(16.dp)) {
|
||||
RadioConfigItemList(
|
||||
state = state,
|
||||
isManaged = localConfig.security.isManaged,
|
||||
excludedModulesUnlocked = excludedModulesUnlocked,
|
||||
onRouteClick = { route ->
|
||||
isWaiting = true
|
||||
viewModel.setResponseStateLoading(route)
|
||||
},
|
||||
onImport = {
|
||||
viewModel.clearPacketResponse()
|
||||
deviceProfile = null
|
||||
val intent =
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/*"
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = stringResource(R.string.bottom_nav_settings),
|
||||
ourNode = ourNode,
|
||||
isConnected = isConnected,
|
||||
showNodeChip = ourNode != null && isConnected,
|
||||
canNavigateUp = false,
|
||||
onNavigateUp = {},
|
||||
actions = {},
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num)
|
||||
else -> {}
|
||||
}
|
||||
importConfigLauncher.launch(intent)
|
||||
},
|
||||
onExport = {
|
||||
viewModel.clearPacketResponse()
|
||||
deviceProfile = null
|
||||
showEditDeviceProfileDialog = true
|
||||
},
|
||||
onNavigate = onNavigate,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(paddingValues).padding(16.dp)) {
|
||||
RadioConfigItemList(
|
||||
state = state,
|
||||
isManaged = localConfig.security.isManaged,
|
||||
excludedModulesUnlocked = excludedModulesUnlocked,
|
||||
onRouteClick = { route ->
|
||||
isWaiting = true
|
||||
viewModel.setResponseStateLoading(route)
|
||||
},
|
||||
onImport = {
|
||||
viewModel.clearPacketResponse()
|
||||
deviceProfile = null
|
||||
val intent =
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/*"
|
||||
}
|
||||
importConfigLauncher.launch(intent)
|
||||
},
|
||||
onExport = {
|
||||
viewModel.clearPacketResponse()
|
||||
deviceProfile = null
|
||||
showEditDeviceProfileDialog = true
|
||||
},
|
||||
onNavigate = onNavigate,
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
val context = LocalContext.current
|
||||
|
||||
TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
|
||||
if (state.analyticsAvailable) {
|
||||
SettingsItemSwitch(
|
||||
text = stringResource(R.string.analytics_okay),
|
||||
checked = state.analyticsEnabled,
|
||||
leadingIcon = Icons.Default.BugReport,
|
||||
onClick = { viewModel.toggleAnalytics() },
|
||||
)
|
||||
}
|
||||
TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
|
||||
if (state.analyticsAvailable) {
|
||||
SettingsItemSwitch(
|
||||
text = stringResource(R.string.analytics_okay),
|
||||
checked = state.analyticsEnabled,
|
||||
leadingIcon = Icons.Default.BugReport,
|
||||
onClick = { viewModel.toggleAnalytics() },
|
||||
)
|
||||
}
|
||||
|
||||
val locationPermissionsState =
|
||||
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
|
||||
val isGpsDisabled = context.gpsDisabled()
|
||||
val provideLocation by uiViewModel.provideLocation.collectAsState(false)
|
||||
val locationPermissionsState =
|
||||
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
|
||||
val isGpsDisabled = context.gpsDisabled()
|
||||
val provideLocation by uiViewModel.provideLocation.collectAsState(false)
|
||||
|
||||
LaunchedEffect(provideLocation, locationPermissionsState.allPermissionsGranted, isGpsDisabled) {
|
||||
if (provideLocation) {
|
||||
if (locationPermissionsState.allPermissionsGranted) {
|
||||
if (!isGpsDisabled) {
|
||||
uiViewModel.meshService?.startProvideLocation()
|
||||
LaunchedEffect(provideLocation, locationPermissionsState.allPermissionsGranted, isGpsDisabled) {
|
||||
if (provideLocation) {
|
||||
if (locationPermissionsState.allPermissionsGranted) {
|
||||
if (!isGpsDisabled) {
|
||||
uiViewModel.meshService?.startProvideLocation()
|
||||
} else {
|
||||
uiViewModel.showSnackBar(context.getString(R.string.location_disabled))
|
||||
}
|
||||
} else {
|
||||
uiViewModel.showSnackBar(context.getString(R.string.location_disabled))
|
||||
// Request permissions if not granted and user wants to provide location
|
||||
locationPermissionsState.launchMultiplePermissionRequest()
|
||||
}
|
||||
} else {
|
||||
// Request permissions if not granted and user wants to provide location
|
||||
locationPermissionsState.launchMultiplePermissionRequest()
|
||||
}
|
||||
} else {
|
||||
uiViewModel.meshService?.stopProvideLocation()
|
||||
}
|
||||
}
|
||||
|
||||
SettingsItemSwitch(
|
||||
text = stringResource(R.string.provide_location_to_mesh),
|
||||
leadingIcon = Icons.Rounded.LocationOn,
|
||||
enabled = !isGpsDisabled,
|
||||
checked = provideLocation,
|
||||
) {
|
||||
uiViewModel.setProvideLocation(!provideLocation)
|
||||
}
|
||||
|
||||
val languageTags = remember { LanguageUtils.getLanguageTags(context) }
|
||||
SettingsItem(
|
||||
text = stringResource(R.string.preferences_language),
|
||||
leadingIcon = Icons.Rounded.Language,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
val lang = LanguageUtils.getLocale()
|
||||
debug("Lang from prefs: $lang")
|
||||
val langMap = languageTags.mapValues { (_, value) -> { LanguageUtils.setLocale(value) } }
|
||||
|
||||
uiViewModel.showAlert(
|
||||
title = context.getString(R.string.preferences_language),
|
||||
message = "",
|
||||
choices = langMap,
|
||||
)
|
||||
}
|
||||
|
||||
val themeMap = remember {
|
||||
mapOf(
|
||||
context.getString(R.string.dynamic) to MODE_DYNAMIC,
|
||||
context.getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO,
|
||||
context.getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES,
|
||||
context.getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
|
||||
)
|
||||
}
|
||||
SettingsItem(
|
||||
text = stringResource(R.string.theme),
|
||||
leadingIcon = Icons.Rounded.FormatPaint,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
uiViewModel.showAlert(
|
||||
title = context.getString(R.string.choose_theme),
|
||||
message = "",
|
||||
choices = themeMap.mapValues { (_, value) -> { uiViewModel.setTheme(value) } },
|
||||
)
|
||||
}
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
|
||||
val exportRangeTestLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) }
|
||||
uiViewModel.meshService?.stopProvideLocation()
|
||||
}
|
||||
}
|
||||
SettingsItem(
|
||||
text = stringResource(R.string.save_rangetest),
|
||||
leadingIcon = Icons.Rounded.Output,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/csv"
|
||||
putExtra(Intent.EXTRA_TITLE, "Meshtastic_rangetest_$timestamp.csv")
|
||||
}
|
||||
exportRangeTestLauncher.launch(intent)
|
||||
}
|
||||
|
||||
val exportDataLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) }
|
||||
}
|
||||
SettingsItemSwitch(
|
||||
text = stringResource(R.string.provide_location_to_mesh),
|
||||
leadingIcon = Icons.Rounded.LocationOn,
|
||||
enabled = !isGpsDisabled,
|
||||
checked = provideLocation,
|
||||
) {
|
||||
uiViewModel.setProvideLocation(!provideLocation)
|
||||
}
|
||||
SettingsItem(
|
||||
text = stringResource(R.string.export_data_csv),
|
||||
leadingIcon = Icons.Rounded.Output,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/csv"
|
||||
putExtra(Intent.EXTRA_TITLE, "Meshtastic_datalog_$timestamp.csv")
|
||||
|
||||
val languageTags = remember { LanguageUtils.getLanguageTags(context) }
|
||||
SettingsItem(
|
||||
text = stringResource(R.string.preferences_language),
|
||||
leadingIcon = Icons.Rounded.Language,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
val lang = LanguageUtils.getLocale()
|
||||
debug("Lang from prefs: $lang")
|
||||
val langMap = languageTags.mapValues { (_, value) -> { LanguageUtils.setLocale(value) } }
|
||||
|
||||
uiViewModel.showAlert(
|
||||
title = context.getString(R.string.preferences_language),
|
||||
message = "",
|
||||
choices = langMap,
|
||||
)
|
||||
}
|
||||
|
||||
val themeMap = remember {
|
||||
mapOf(
|
||||
context.getString(R.string.dynamic) to MODE_DYNAMIC,
|
||||
context.getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO,
|
||||
context.getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES,
|
||||
context.getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
|
||||
)
|
||||
}
|
||||
SettingsItem(
|
||||
text = stringResource(R.string.theme),
|
||||
leadingIcon = Icons.Rounded.FormatPaint,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
uiViewModel.showAlert(
|
||||
title = context.getString(R.string.choose_theme),
|
||||
message = "",
|
||||
choices = themeMap.mapValues { (_, value) -> { uiViewModel.setTheme(value) } },
|
||||
)
|
||||
}
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
|
||||
val exportRangeTestLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) }
|
||||
}
|
||||
}
|
||||
exportDataLauncher.launch(intent)
|
||||
}
|
||||
SettingsItem(
|
||||
text = stringResource(R.string.save_rangetest),
|
||||
leadingIcon = Icons.Rounded.Output,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/csv"
|
||||
putExtra(Intent.EXTRA_TITLE, "Meshtastic_rangetest_$timestamp.csv")
|
||||
}
|
||||
exportRangeTestLauncher.launch(intent)
|
||||
}
|
||||
|
||||
SettingsItem(
|
||||
text = stringResource(R.string.intro_show),
|
||||
leadingIcon = Icons.Rounded.WavingHand,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
uiViewModel.showAppIntro()
|
||||
}
|
||||
val exportDataLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) }
|
||||
}
|
||||
}
|
||||
SettingsItem(
|
||||
text = stringResource(R.string.export_data_csv),
|
||||
leadingIcon = Icons.Rounded.Output,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/csv"
|
||||
putExtra(Intent.EXTRA_TITLE, "Meshtastic_datalog_$timestamp.csv")
|
||||
}
|
||||
exportDataLauncher.launch(intent)
|
||||
}
|
||||
|
||||
AppVersionButton(excludedModulesUnlocked) { uiViewModel.unlockExcludedModules() }
|
||||
SettingsItem(
|
||||
text = stringResource(R.string.intro_show),
|
||||
leadingIcon = Icons.Rounded.WavingHand,
|
||||
trailingIcon = null,
|
||||
) {
|
||||
uiViewModel.showAppIntro()
|
||||
}
|
||||
|
||||
AppVersionButton(excludedModulesUnlocked) { uiViewModel.unlockExcludedModules() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user