Refactor: Settings to Connections, ui updates (#1984)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
pull/1975/head
James Rich 2025-05-30 12:56:49 -05:00 zatwierdzone przez GitHub
rodzic ad1897c564
commit b861d07aba
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
5 zmienionych plików z 137 dodań i 88 usunięć

Wyświetl plik

@ -32,13 +32,13 @@ import androidx.navigation.toRoute
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
import com.geeksville.mesh.ui.connections.ConnectionsScreen
import com.geeksville.mesh.ui.contact.ContactsScreen import com.geeksville.mesh.ui.contact.ContactsScreen
import com.geeksville.mesh.ui.debug.DebugScreen import com.geeksville.mesh.ui.debug.DebugScreen
import com.geeksville.mesh.ui.map.MapView import com.geeksville.mesh.ui.map.MapView
import com.geeksville.mesh.ui.message.MessageScreen import com.geeksville.mesh.ui.message.MessageScreen
import com.geeksville.mesh.ui.message.QuickChatScreen import com.geeksville.mesh.ui.message.QuickChatScreen
import com.geeksville.mesh.ui.node.NodeScreen import com.geeksville.mesh.ui.node.NodeScreen
import com.geeksville.mesh.ui.settings.SettingsScreen
import com.geeksville.mesh.ui.sharing.ChannelScreen import com.geeksville.mesh.ui.sharing.ChannelScreen
import com.geeksville.mesh.ui.sharing.ShareScreen import com.geeksville.mesh.ui.sharing.ShareScreen
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -76,7 +76,7 @@ sealed interface Route {
data object Channels : Route data object Channels : Route
@Serializable @Serializable
data object Settings : Route data object Connections : Route
@Serializable @Serializable
data object DebugPanel : Route data object DebugPanel : Route
@ -219,7 +219,7 @@ fun NavGraph(
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = if (uIViewModel.bondedAddress.isNullOrBlank()) { startDestination = if (uIViewModel.bondedAddress.isNullOrBlank()) {
Route.Settings Route.Connections
} else { } else {
Route.Contacts Route.Contacts
}, },
@ -244,19 +244,19 @@ fun NavGraph(
composable<Route.Channels> { composable<Route.Channels> {
ChannelScreen(uIViewModel) ChannelScreen(uIViewModel)
} }
composable<Route.Settings>( composable<Route.Connections>(
deepLinks = listOf( deepLinks = listOf(
navDeepLink { navDeepLink {
uriPattern = "$DEEP_LINK_BASE_URI/settings" uriPattern = "$DEEP_LINK_BASE_URI/connections"
action = "android.intent.action.VIEW" action = "android.intent.action.VIEW"
} }
) )
) { backStackEntry -> ) { backStackEntry ->
SettingsScreen( ConnectionsScreen(
uIViewModel, uIViewModel,
onNavigateToRadioConfig = { onNavigateToRadioConfig = {
navController.navigate(Route.RadioConfig()) { navController.navigate(Route.RadioConfig()) {
popUpTo(Route.Settings) { popUpTo(Route.Connections) {
inclusive = false inclusive = false
} }
} }

Wyświetl plik

@ -17,12 +17,7 @@
package com.geeksville.mesh.ui package com.geeksville.mesh.ui
import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.safeDrawingPadding
@ -36,7 +31,6 @@ import androidx.compose.material.icons.twotone.CloudUpload
import androidx.compose.material.icons.twotone.Contactless import androidx.compose.material.icons.twotone.Contactless
import androidx.compose.material.icons.twotone.Map import androidx.compose.material.icons.twotone.Map
import androidx.compose.material.icons.twotone.People import androidx.compose.material.icons.twotone.People
import androidx.compose.material.icons.twotone.Settings
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -45,10 +39,14 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@ -56,8 +54,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -82,12 +80,12 @@ import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
import com.geeksville.mesh.ui.debug.DebugMenuActions import com.geeksville.mesh.ui.debug.DebugMenuActions
enum class TopLevelDestination(val label: String, val icon: ImageVector, val route: Route) { enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, val route: Route) {
Contacts("Contacts", Icons.AutoMirrored.TwoTone.Chat, Route.Contacts), Contacts(R.string.contacts, Icons.AutoMirrored.TwoTone.Chat, Route.Contacts),
Nodes("Nodes", Icons.TwoTone.People, Route.Nodes), Nodes(R.string.nodes, Icons.TwoTone.People, Route.Nodes),
Map("Map", Icons.TwoTone.Map, Route.Map), Map(R.string.map, Icons.TwoTone.Map, Route.Map),
Channels("Channels", Icons.TwoTone.Contactless, Route.Channels), Channels(R.string.channels, Icons.TwoTone.Contactless, Route.Channels),
Settings("Settings", Icons.TwoTone.Settings, Route.Settings), Connections(R.string.connections, Icons.TwoTone.CloudOff, Route.Connections),
; ;
companion object { companion object {
@ -109,7 +107,6 @@ fun MainScreen(
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
val localConfig by viewModel.localConfig.collectAsStateWithLifecycle() val localConfig by viewModel.localConfig.collectAsStateWithLifecycle()
val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle() val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle()
if (connectionState.isConnected()) { if (connectionState.isConnected()) {
requestChannelSet?.let { newChannelSet -> requestChannelSet?.let { newChannelSet ->
ScannedQrCodeDialog(viewModel, newChannelSet) ScannedQrCodeDialog(viewModel, newChannelSet)
@ -154,7 +151,6 @@ fun MainScreen(
MainAppBar( MainAppBar(
title = title, title = title,
isManaged = localConfig.security.isManaged, isManaged = localConfig.security.isManaged,
connectionState = connectionState,
navController = navController, navController = navController,
) { action -> ) { action ->
when (action) { when (action) {
@ -167,6 +163,7 @@ fun MainScreen(
}, },
bottomBar = { bottomBar = {
BottomNavigation( BottomNavigation(
connectionState = connectionState,
navController = navController, navController = navController,
) )
}, },
@ -197,7 +194,6 @@ enum class MainMenuAction(@StringRes val stringRes: Int) {
private fun MainAppBar( private fun MainAppBar(
title: String, title: String,
isManaged: Boolean, isManaged: Boolean,
connectionState: MeshService.ConnectionState,
navController: NavHostController, navController: NavHostController,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onAction: (MainMenuAction) -> Unit onAction: (MainMenuAction) -> Unit
@ -267,7 +263,7 @@ private fun MainAppBar(
actions = { actions = {
when { when {
currentDestination == null || isTopLevelRoute -> currentDestination == null || isTopLevelRoute ->
MainMenuActions(isManaged, connectionState, onAction) MainMenuActions(isManaged, onAction)
currentDestination.hasRoute<Route.DebugPanel>() -> currentDestination.hasRoute<Route.DebugPanel>() ->
DebugMenuActions() DebugMenuActions()
@ -278,30 +274,13 @@ private fun MainAppBar(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun MainMenuActions( private fun MainMenuActions(
isManaged: Boolean, isManaged: Boolean,
connectionState: MeshService.ConnectionState,
onAction: (MainMenuAction) -> Unit onAction: (MainMenuAction) -> Unit
) { ) {
val context = LocalContext.current
val (image, tooltip) = when (connectionState) {
MeshService.ConnectionState.CONNECTED -> Icons.TwoTone.CloudDone to R.string.connected
MeshService.ConnectionState.DEVICE_SLEEP -> Icons.TwoTone.CloudUpload to R.string.device_sleeping
MeshService.ConnectionState.DISCONNECTED -> Icons.TwoTone.CloudOff to R.string.disconnected
}
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
IconButton(
onClick = {
Toast.makeText(context, tooltip, Toast.LENGTH_SHORT).show()
},
) {
Icon(
imageVector = image,
contentDescription = stringResource(id = tooltip),
)
}
IconButton(onClick = { showMenu = true }) { IconButton(onClick = { showMenu = true }) {
Icon( Icon(
imageVector = Icons.Default.MoreVert, imageVector = Icons.Default.MoreVert,
@ -330,39 +309,43 @@ private fun MainMenuActions(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun BottomNavigation( private fun BottomNavigation(
connectionState: MeshService.ConnectionState,
navController: NavController, navController: NavController,
) { ) {
val currentDestination = navController.currentBackStackEntryAsState().value?.destination val currentDestination = navController.currentBackStackEntryAsState().value?.destination
val topLevelDestination = TopLevelDestination.fromNavDestination(currentDestination) val topLevelDestination = TopLevelDestination.fromNavDestination(currentDestination)
AnimatedVisibility(
visible = topLevelDestination != null,
enter = slideInVertically(
initialOffsetY = { it / 2 },
animationSpec = tween(durationMillis = 50),
),
exit = slideOutVertically(
targetOffsetY = { it / 2 },
animationSpec = tween(durationMillis = 50),
),
) {
NavigationBar { NavigationBar {
TopLevelDestination.entries.forEach { TopLevelDestination.entries.forEach { destination ->
val isSelected = it == topLevelDestination val isSelected = destination == topLevelDestination
val isConnectionsRoute = destination == TopLevelDestination.Connections
NavigationBarItem( NavigationBarItem(
icon = { icon = {
Icon( TooltipBox(
imageVector = it.icon, positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
contentDescription = it.name, tooltip = {
) PlainTooltip {
Text(
if (isConnectionsRoute) {
connectionState.getTooltipString()
} else {
stringResource(id = destination.label)
},
)
}
},
state = rememberTooltipState()
) {
TopLevelNavIcon(destination, connectionState)
}
}, },
// label = { Text(it.label) },
selected = isSelected, selected = isSelected,
onClick = { onClick = {
if (!isSelected) { if (!isSelected) {
navController.navigate(it.route) { navController.navigate(destination.route) {
// Pop up to the start destination of the graph to // Pop up to the start destination of the graph to
// avoid building up a large stack of destinations // avoid building up a large stack of destinations
// on the back stack as users select items // on the back stack as users select items
@ -380,5 +363,49 @@ private fun BottomNavigation(
) )
} }
} }
}
@Composable
private fun MeshService.ConnectionState.getConnectionColor(): Color {
return when (this) {
MeshService.ConnectionState.CONNECTED -> Color(color = 0xFF30C047)
MeshService.ConnectionState.DEVICE_SLEEP -> MaterialTheme.colorScheme.tertiary
MeshService.ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.error
}
}
private fun MeshService.ConnectionState.getConnectionIcon(): ImageVector {
return when (this) {
MeshService.ConnectionState.CONNECTED -> Icons.TwoTone.CloudDone
MeshService.ConnectionState.DEVICE_SLEEP -> Icons.TwoTone.CloudUpload
MeshService.ConnectionState.DISCONNECTED -> Icons.TwoTone.CloudOff
}
}
@Composable
private fun MeshService.ConnectionState.getTooltipString(): String {
return when (this) {
MeshService.ConnectionState.CONNECTED -> stringResource(R.string.connected)
MeshService.ConnectionState.DEVICE_SLEEP -> stringResource(R.string.device_sleeping)
MeshService.ConnectionState.DISCONNECTED -> stringResource(R.string.disconnected)
}
}
@Composable
private fun TopLevelNavIcon(
dest: TopLevelDestination,
connectionState: MeshService.ConnectionState
) {
when (dest) {
TopLevelDestination.Connections -> Icon(
imageVector = connectionState.getConnectionIcon(),
contentDescription = stringResource(id = dest.label),
tint = connectionState.getConnectionColor(),
)
else -> Icon(
imageVector = dest.icon,
contentDescription = stringResource(id = dest.label),
)
} }
} }

Wyświetl plik

@ -21,7 +21,8 @@ package com.geeksville.mesh.ui.common.theme
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
@ -271,10 +272,10 @@ val unspecified_scheme = ColorFamily(
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
) )
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun AppTheme( fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true, dynamicColor: Boolean = true,
content: @Composable() () -> Unit content: @Composable() () -> Unit
) { ) {
@ -288,7 +289,7 @@ fun AppTheme(
else -> lightScheme else -> lightScheme
} }
MaterialTheme( MaterialExpressiveTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = AppTypography, typography = AppTypography,
content = content content = content

Wyświetl plik

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.geeksville.mesh.ui.settings package com.geeksville.mesh.ui.connections
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
@ -108,9 +108,9 @@ fun String?.isIPAddress(): Boolean {
} }
} }
@Suppress("CyclomaticComplexMethod", "LongMethod") @Suppress("CyclomaticComplexMethod", "LongMethod", "MagicNumber")
@Composable @Composable
fun SettingsScreen( fun ConnectionsScreen(
uiViewModel: UIViewModel = hiltViewModel(), uiViewModel: UIViewModel = hiltViewModel(),
scanModel: BTScanModel = hiltViewModel(), scanModel: BTScanModel = hiltViewModel(),
bluetoothViewModel: BluetoothViewModel = hiltViewModel(), bluetoothViewModel: BluetoothViewModel = hiltViewModel(),
@ -389,7 +389,7 @@ fun SettingsScreen(
label = { Text(stringResource(R.string.ip_address)) }, label = { Text(stringResource(R.string.ip_address)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier modifier = Modifier
.weight(1f) .weight(0.7f)
.padding(start = 16.dp) .padding(start = 16.dp)
) )
OutlinedTextField( OutlinedTextField(

Wyświetl plik

@ -1,3 +1,20 @@
<!--
~ Copyright (c) 2025 Meshtastic LLC
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources> <resources>
// Language tags native names (not available via .getDisplayLanguage) // Language tags native names (not available via .getDisplayLanguage)
<string name="fr_HT" translatable="false">Kreyòl ayisyen</string> <string name="fr_HT" translatable="false">Kreyòl ayisyen</string>
@ -638,4 +655,8 @@
<string name="disk_free">Disk Free</string> <string name="disk_free">Disk Free</string>
<string name="load">Load</string> <string name="load">Load</string>
<string name="user_string">User String</string> <string name="user_string">User String</string>
<string name="connections">Connections</string>
<string name="map">Map</string>
<string name="contacts">Contacts</string>
<string name="nodes">Nodes</string>
</resources> </resources>