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

Wyświetl plik

@ -17,12 +17,7 @@
package com.geeksville.mesh.ui
import android.widget.Toast
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.layout.padding
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.Map
import androidx.compose.material.icons.twotone.People
import androidx.compose.material.icons.twotone.Settings
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@ -45,10 +39,14 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@ -56,8 +54,8 @@ 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.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
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.debug.DebugMenuActions
enum class TopLevelDestination(val label: String, val icon: ImageVector, val route: Route) {
Contacts("Contacts", Icons.AutoMirrored.TwoTone.Chat, Route.Contacts),
Nodes("Nodes", Icons.TwoTone.People, Route.Nodes),
Map("Map", Icons.TwoTone.Map, Route.Map),
Channels("Channels", Icons.TwoTone.Contactless, Route.Channels),
Settings("Settings", Icons.TwoTone.Settings, Route.Settings),
enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, val route: Route) {
Contacts(R.string.contacts, Icons.AutoMirrored.TwoTone.Chat, Route.Contacts),
Nodes(R.string.nodes, Icons.TwoTone.People, Route.Nodes),
Map(R.string.map, Icons.TwoTone.Map, Route.Map),
Channels(R.string.channels, Icons.TwoTone.Contactless, Route.Channels),
Connections(R.string.connections, Icons.TwoTone.CloudOff, Route.Connections),
;
companion object {
@ -109,7 +107,6 @@ fun MainScreen(
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
val localConfig by viewModel.localConfig.collectAsStateWithLifecycle()
val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle()
if (connectionState.isConnected()) {
requestChannelSet?.let { newChannelSet ->
ScannedQrCodeDialog(viewModel, newChannelSet)
@ -154,7 +151,6 @@ fun MainScreen(
MainAppBar(
title = title,
isManaged = localConfig.security.isManaged,
connectionState = connectionState,
navController = navController,
) { action ->
when (action) {
@ -167,6 +163,7 @@ fun MainScreen(
},
bottomBar = {
BottomNavigation(
connectionState = connectionState,
navController = navController,
)
},
@ -197,7 +194,6 @@ enum class MainMenuAction(@StringRes val stringRes: Int) {
private fun MainAppBar(
title: String,
isManaged: Boolean,
connectionState: MeshService.ConnectionState,
navController: NavHostController,
modifier: Modifier = Modifier,
onAction: (MainMenuAction) -> Unit
@ -267,7 +263,7 @@ private fun MainAppBar(
actions = {
when {
currentDestination == null || isTopLevelRoute ->
MainMenuActions(isManaged, connectionState, onAction)
MainMenuActions(isManaged, onAction)
currentDestination.hasRoute<Route.DebugPanel>() ->
DebugMenuActions()
@ -278,30 +274,13 @@ private fun MainAppBar(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MainMenuActions(
isManaged: Boolean,
connectionState: MeshService.ConnectionState,
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) }
IconButton(
onClick = {
Toast.makeText(context, tooltip, Toast.LENGTH_SHORT).show()
},
) {
Icon(
imageVector = image,
contentDescription = stringResource(id = tooltip),
)
}
IconButton(onClick = { showMenu = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
@ -330,55 +309,103 @@ private fun MainMenuActions(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun BottomNavigation(
connectionState: MeshService.ConnectionState,
navController: NavController,
) {
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
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 {
TopLevelDestination.entries.forEach {
val isSelected = it == topLevelDestination
NavigationBarItem(
icon = {
Icon(
imageVector = it.icon,
contentDescription = it.name,
)
},
// label = { Text(it.label) },
selected = isSelected,
onClick = {
if (!isSelected) {
navController.navigate(it.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
NavigationBar {
TopLevelDestination.entries.forEach { destination ->
val isSelected = destination == topLevelDestination
val isConnectionsRoute = destination == TopLevelDestination.Connections
NavigationBarItem(
icon = {
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(
if (isConnectionsRoute) {
connectionState.getTooltipString()
} else {
stringResource(id = destination.label)
},
)
}
},
state = rememberTooltipState()
) {
TopLevelNavIcon(destination, connectionState)
}
},
selected = isSelected,
onClick = {
if (!isSelected) {
navController.navigate(destination.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
)
}
}
}
@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 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.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
@ -271,10 +272,10 @@ val unspecified_scheme = ColorFamily(
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable() () -> Unit
) {
@ -288,7 +289,7 @@ fun AppTheme(
else -> lightScheme
}
MaterialTheme(
MaterialExpressiveTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content

Wyświetl plik

@ -15,7 +15,7 @@
* 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.content.Context
@ -108,9 +108,9 @@ fun String?.isIPAddress(): Boolean {
}
}
@Suppress("CyclomaticComplexMethod", "LongMethod")
@Suppress("CyclomaticComplexMethod", "LongMethod", "MagicNumber")
@Composable
fun SettingsScreen(
fun ConnectionsScreen(
uiViewModel: UIViewModel = hiltViewModel(),
scanModel: BTScanModel = hiltViewModel(),
bluetoothViewModel: BluetoothViewModel = hiltViewModel(),
@ -389,7 +389,7 @@ fun SettingsScreen(
label = { Text(stringResource(R.string.ip_address)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier
.weight(1f)
.weight(0.7f)
.padding(start = 16.dp)
)
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>
// Language tags native names (not available via .getDisplayLanguage)
<string name="fr_HT" translatable="false">Kreyòl ayisyen</string>
@ -638,4 +655,8 @@
<string name="disk_free">Disk Free</string>
<string name="load">Load</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>