mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(ui): add mesh activity modem lights (#2656)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
da42d67486
commit
ad59edd8d2
3 changed files with 206 additions and 84 deletions
|
|
@ -60,6 +60,7 @@ import com.geeksville.mesh.repository.api.DeviceHardwareRepository
|
|||
import com.geeksville.mesh.repository.api.FirmwareReleaseRepository
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import com.geeksville.mesh.repository.location.LocationRepository
|
||||
import com.geeksville.mesh.repository.radio.MeshActivity
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.service.MeshServiceNotifications
|
||||
|
|
@ -72,6 +73,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -84,6 +86,7 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -239,6 +242,13 @@ constructor(
|
|||
meshServiceNotifications.clearClientNotification(notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits events for mesh network send/receive activity. This is a SharedFlow to ensure all events are delivered,
|
||||
* even if they are the same.
|
||||
*/
|
||||
val meshActivity: SharedFlow<MeshActivity> =
|
||||
radioInterfaceService.meshActivity.shareIn(viewModelScope, SharingStarted.Eagerly, 0)
|
||||
|
||||
data class AlertData(
|
||||
val title: String,
|
||||
val message: String? = null,
|
||||
|
|
|
|||
|
|
@ -39,10 +39,12 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -51,16 +53,18 @@ import javax.inject.Inject
|
|||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Handles the bluetooth link with a mesh radio device. Does not cache any device state,
|
||||
* just does bluetooth comms etc...
|
||||
* Handles the bluetooth link with a mesh radio device. Does not cache any device state, just does bluetooth comms
|
||||
* etc...
|
||||
*
|
||||
* This service is not exposed outside of this process.
|
||||
*
|
||||
* Note - this class intentionally dumb. It doesn't understand protobuf framing etc...
|
||||
* It is designed to be simple so it can be stubbed out with a simulated version as needed.
|
||||
* Note - this class intentionally dumb. It doesn't understand protobuf framing etc... It is designed to be simple so it
|
||||
* can be stubbed out with a simulated version as needed.
|
||||
*/
|
||||
@Singleton
|
||||
class RadioInterfaceService @Inject constructor(
|
||||
class RadioInterfaceService
|
||||
@Inject
|
||||
constructor(
|
||||
private val context: Application,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val bluetoothRepository: BluetoothRepository,
|
||||
|
|
@ -85,18 +89,15 @@ class RadioInterfaceService @Inject constructor(
|
|||
private lateinit var sentPacketsLog: BinaryLogFile // inited in onCreate
|
||||
private lateinit var receivedPacketsLog: BinaryLogFile
|
||||
|
||||
val mockInterfaceAddress: String by lazy {
|
||||
toInterfaceAddress(InterfaceId.MOCK, "")
|
||||
}
|
||||
val mockInterfaceAddress: String by lazy { toInterfaceAddress(InterfaceId.MOCK, "") }
|
||||
|
||||
/**
|
||||
* We recreate this scope each time we stop an interface
|
||||
*/
|
||||
/** We recreate this scope each time we stop an interface */
|
||||
var serviceScope = CoroutineScope(Dispatchers.IO + Job())
|
||||
|
||||
private var radioIf: IRadioInterface = NopInterface("")
|
||||
|
||||
/** true if we have started our interface
|
||||
/**
|
||||
* true if we have started our interface
|
||||
*
|
||||
* Note: an interface may be started without necessarily yet having a connection
|
||||
*/
|
||||
|
|
@ -106,15 +107,25 @@ class RadioInterfaceService @Inject constructor(
|
|||
private var isConnected = false
|
||||
|
||||
private fun initStateListeners() {
|
||||
bluetoothRepository.state.onEach { state ->
|
||||
if (state.enabled) startInterface()
|
||||
else if (radioIf is BluetoothInterface) stopInterface()
|
||||
}.launchIn(processLifecycle.coroutineScope)
|
||||
bluetoothRepository.state
|
||||
.onEach { state ->
|
||||
if (state.enabled) {
|
||||
startInterface()
|
||||
} else if (radioIf is BluetoothInterface) {
|
||||
stopInterface()
|
||||
}
|
||||
}
|
||||
.launchIn(processLifecycle.coroutineScope)
|
||||
|
||||
networkRepository.networkAvailable.onEach { state ->
|
||||
if (state) startInterface()
|
||||
else if (radioIf is TCPInterface) stopInterface()
|
||||
}.launchIn(processLifecycle.coroutineScope)
|
||||
networkRepository.networkAvailable
|
||||
.onEach { state ->
|
||||
if (state) {
|
||||
startInterface()
|
||||
} else if (radioIf is TCPInterface) {
|
||||
stopInterface()
|
||||
}
|
||||
}
|
||||
.launchIn(processLifecycle.coroutineScope)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
@ -123,48 +134,43 @@ class RadioInterfaceService @Inject constructor(
|
|||
}
|
||||
|
||||
private var lastHeartbeatMillis = 0L
|
||||
|
||||
private fun keepAlive(now: Long) {
|
||||
if (now - lastHeartbeatMillis > HEARTBEAT_INTERVAL_MILLIS) {
|
||||
info("Sending ToRadio heartbeat")
|
||||
val heartbeat = MeshProtos.ToRadio.newBuilder()
|
||||
.setHeartbeat(MeshProtos.Heartbeat.getDefaultInstance()).build()
|
||||
val heartbeat =
|
||||
MeshProtos.ToRadio.newBuilder().setHeartbeat(MeshProtos.Heartbeat.getDefaultInstance()).build()
|
||||
handleSendToRadio(heartbeat.toByteArray())
|
||||
lastHeartbeatMillis = now
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a full radio address for the specific interface type.
|
||||
*/
|
||||
fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String {
|
||||
return interfaceFactory.toInterfaceAddress(interfaceId, rest)
|
||||
}
|
||||
/** Constructs a full radio address for the specific interface type. */
|
||||
fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String =
|
||||
interfaceFactory.toInterfaceAddress(interfaceId, rest)
|
||||
|
||||
fun isMockInterface(): Boolean {
|
||||
return BuildConfig.DEBUG || (context as GeeksvilleApplication).isInTestLab
|
||||
}
|
||||
fun isMockInterface(): Boolean = BuildConfig.DEBUG || (context as GeeksvilleApplication).isInTestLab
|
||||
|
||||
/**
|
||||
* Determines whether to default to mock interface for device address.
|
||||
* This keeps the decision logic separate and easy to extend.
|
||||
* Determines whether to default to mock interface for device address. This keeps the decision logic separate and
|
||||
* easy to extend.
|
||||
*/
|
||||
private fun shouldDefaultToMockInterface(): Boolean {
|
||||
return BuildUtils.isEmulator
|
||||
}
|
||||
private fun shouldDefaultToMockInterface(): Boolean = BuildUtils.isEmulator
|
||||
|
||||
/** Return the device we are configured to use, or null for none
|
||||
* device address strings are of the form:
|
||||
/**
|
||||
* Return the device we are configured to use, or null for none device address strings are of the form:
|
||||
*
|
||||
* at
|
||||
*
|
||||
* where a is either x for bluetooth or s for serial
|
||||
* and t is an interface specific address (macaddr or a device path)
|
||||
* where a is either x for bluetooth or s for serial and t is an interface specific address (macaddr or a device
|
||||
* path)
|
||||
*/
|
||||
fun getDeviceAddress(): String? {
|
||||
// If the user has unpaired our device, treat things as if we don't have one
|
||||
var address = prefs.getString(DEVADDR_KEY, null)
|
||||
|
||||
// If we are running on the emulator we default to the mock interface, so we can have some data to show to the user
|
||||
// If we are running on the emulator we default to the mock interface, so we can have some data to show to the
|
||||
// user
|
||||
if (address == null && shouldDefaultToMockInterface()) {
|
||||
address = mockInterfaceAddress
|
||||
}
|
||||
|
|
@ -172,12 +178,13 @@ class RadioInterfaceService @Inject constructor(
|
|||
return address
|
||||
}
|
||||
|
||||
/** Like getDeviceAddress, but filtered to return only devices we are currently bonded with
|
||||
/**
|
||||
* Like getDeviceAddress, but filtered to return only devices we are currently bonded with
|
||||
*
|
||||
* at
|
||||
*
|
||||
* where a is either x for bluetooth or s for serial
|
||||
* and t is an interface specific address (macaddr or a device path)
|
||||
* where a is either x for bluetooth or s for serial and t is an interface specific address (macaddr or a device
|
||||
* path)
|
||||
*/
|
||||
fun getBondedDeviceAddress(): String? {
|
||||
// If the user has unpaired our device, treat things as if we don't have one
|
||||
|
|
@ -193,15 +200,14 @@ class RadioInterfaceService @Inject constructor(
|
|||
debug("Broadcasting connection=$isConnected")
|
||||
|
||||
processLifecycle.coroutineScope.launch(dispatchers.default) {
|
||||
_connectionState.emit(
|
||||
RadioServiceConnectionState(isConnected, isPermanent)
|
||||
)
|
||||
_connectionState.emit(RadioServiceConnectionState(isConnected, isPermanent))
|
||||
}
|
||||
}
|
||||
|
||||
// Send a packet/command out the radio link, this routine can block if it needs to
|
||||
private fun handleSendToRadio(p: ByteArray) {
|
||||
radioIf.handleSendToRadio(p)
|
||||
emitSendActivity()
|
||||
}
|
||||
|
||||
// Handle an incoming packet from the radio, broadcasts it as an android intent
|
||||
|
|
@ -217,9 +223,8 @@ class RadioInterfaceService @Inject constructor(
|
|||
|
||||
// ignoreException { debug("FromRadio: ${MeshProtos.FromRadio.parseFrom(p)}") }
|
||||
|
||||
processLifecycle.coroutineScope.launch(dispatchers.io) {
|
||||
_receivedData.emit(p)
|
||||
}
|
||||
processLifecycle.coroutineScope.launch(dispatchers.io) { _receivedData.emit(p) }
|
||||
emitReceiveActivity()
|
||||
}
|
||||
|
||||
fun onConnect() {
|
||||
|
|
@ -289,20 +294,16 @@ class RadioInterfaceService @Inject constructor(
|
|||
*
|
||||
* @return true if the device changed, false if no change
|
||||
*/
|
||||
private fun setBondedDeviceAddress(address: String?): Boolean {
|
||||
return if (getBondedDeviceAddress() == address && isStarted) {
|
||||
private fun setBondedDeviceAddress(address: String?): Boolean =
|
||||
if (getBondedDeviceAddress() == address && isStarted) {
|
||||
warn("Ignoring setBondedDevice ${address.anonymize}, because we are already using that device")
|
||||
false
|
||||
} else {
|
||||
// Record that this use has configured a new radio
|
||||
GeeksvilleApplication.analytics.track(
|
||||
"mesh_bond"
|
||||
)
|
||||
GeeksvilleApplication.analytics.track("mesh_bond")
|
||||
|
||||
// Ignore any errors that happen while closing old device
|
||||
ignoreException {
|
||||
stopInterface()
|
||||
}
|
||||
ignoreException { stopInterface() }
|
||||
|
||||
// The device address "n" can be used to mean none
|
||||
|
||||
|
|
@ -321,14 +322,13 @@ class RadioInterfaceService @Inject constructor(
|
|||
startInterface()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun setDeviceAddress(deviceAddr: String?): Boolean = toRemoteExceptions {
|
||||
setBondedDeviceAddress(deviceAddr)
|
||||
}
|
||||
fun setDeviceAddress(deviceAddr: String?): Boolean = toRemoteExceptions { setBondedDeviceAddress(deviceAddr) }
|
||||
|
||||
/** If the service is not currently connected to the radio, try to connect now. At boot the radio interface service will
|
||||
* not connect to a radio until this call is received. */
|
||||
/**
|
||||
* If the service is not currently connected to the radio, try to connect now. At boot the radio interface service
|
||||
* will not connect to a radio until this call is received.
|
||||
*/
|
||||
fun connect() = toRemoteExceptions {
|
||||
// We don't start actually talking to our device until MeshService binds to us - this prevents
|
||||
// broadcasting connection events before MeshService is ready to receive them
|
||||
|
|
@ -340,4 +340,33 @@ class RadioInterfaceService @Inject constructor(
|
|||
// Do this in the IO thread because it might take a while (and we don't care about the result code)
|
||||
serviceScope.handledLaunch { handleSendToRadio(a) }
|
||||
}
|
||||
|
||||
private val _meshActivity =
|
||||
MutableSharedFlow<MeshActivity>(
|
||||
replay = 0, // No replay needed for event-like emissions
|
||||
extraBufferCapacity = 1, // Buffer one event to avoid loss on rapid emissions
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST, // Drop oldest if buffer overflows
|
||||
)
|
||||
val meshActivity: SharedFlow<MeshActivity> = _meshActivity.asSharedFlow()
|
||||
|
||||
private fun emitSendActivity() {
|
||||
// Use tryEmit for SharedFlow as it's non-blocking
|
||||
val emitted = _meshActivity.tryEmit(MeshActivity.Send)
|
||||
if (!emitted) {
|
||||
debug("MeshActivity.Send event was not emitted due to buffer overflow or no collectors")
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitReceiveActivity() {
|
||||
val emitted = _meshActivity.tryEmit(MeshActivity.Receive)
|
||||
if (!emitted) {
|
||||
debug("MeshActivity.Receive event was not emitted due to buffer overflow or no collectors")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class MeshActivity {
|
||||
data object Send : MeshActivity()
|
||||
|
||||
data object Receive : MeshActivity()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ 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
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
|
|
@ -43,6 +47,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
|
|
@ -57,13 +62,16 @@ import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
|
|||
import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
|
@ -98,12 +106,14 @@ import com.geeksville.mesh.navigation.NodesRoutes
|
|||
import com.geeksville.mesh.navigation.RadioConfigRoutes
|
||||
import com.geeksville.mesh.navigation.Route
|
||||
import com.geeksville.mesh.navigation.showLongNameTitle
|
||||
import com.geeksville.mesh.repository.radio.MeshActivity
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
|
||||
import com.geeksville.mesh.ui.common.components.MultipleChoiceAlertDialog
|
||||
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
|
||||
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
|
||||
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
|
||||
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusOrange
|
||||
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
|
||||
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
|
||||
import com.geeksville.mesh.ui.debug.DebugMenuActions
|
||||
|
|
@ -114,6 +124,8 @@ import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
|||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, val route: Route) {
|
||||
Contacts(R.string.contacts, Icons.AutoMirrored.TwoTone.Chat, ContactsRoutes.ContactsGraph),
|
||||
|
|
@ -222,6 +234,39 @@ fun MainScreen(
|
|||
val navSuiteType = NavigationSuiteScaffoldDefaults.navigationSuiteType(currentWindowAdaptiveInfo())
|
||||
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
|
||||
val topLevelDestination = TopLevelDestination.fromNavDestination(currentDestination)
|
||||
|
||||
// State for managing the glow animation around the Connections icon
|
||||
var currentGlowColor by remember { mutableStateOf(Color.Transparent) }
|
||||
val animatedGlowAlpha = remember { Animatable(0f) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val capturedColorScheme = colorScheme // Capture current colorScheme instance for LaunchedEffect
|
||||
|
||||
val sendColor = capturedColorScheme.StatusOrange
|
||||
val receiveColor = capturedColorScheme.StatusYellow
|
||||
LaunchedEffect(uIViewModel.meshActivity, capturedColorScheme) {
|
||||
uIViewModel.meshActivity.collectLatest { activity ->
|
||||
debug("MeshActivity Event: $activity, Current Alpha: ${animatedGlowAlpha.value}")
|
||||
|
||||
val newTargetColor =
|
||||
when (activity) {
|
||||
is MeshActivity.Send -> sendColor
|
||||
is MeshActivity.Receive -> receiveColor
|
||||
}
|
||||
|
||||
currentGlowColor = newTargetColor
|
||||
// Stop any existing animation and launch a new one.
|
||||
// Launching in a new coroutine ensures the collect block is not suspended.
|
||||
coroutineScope.launch {
|
||||
animatedGlowAlpha.stop() // Stop before snapping/animating
|
||||
animatedGlowAlpha.snapTo(1.0f) // Show glow instantly
|
||||
animatedGlowAlpha.animateTo(
|
||||
targetValue = 0.0f, // Fade out
|
||||
animationSpec = tween(durationMillis = 1000, easing = LinearEasing),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavigationSuiteScaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
navigationSuiteItems = {
|
||||
|
|
@ -245,7 +290,39 @@ fun MainScreen(
|
|||
},
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
TopLevelNavIcon(destination, connectionState)
|
||||
val iconModifier =
|
||||
if (isConnectionsRoute) {
|
||||
Modifier.drawWithCache {
|
||||
onDrawWithContent {
|
||||
drawContent()
|
||||
if (animatedGlowAlpha.value > 0f) {
|
||||
val glowRadius = size.minDimension
|
||||
drawCircle(
|
||||
brush =
|
||||
Brush.radialGradient(
|
||||
colors =
|
||||
listOf(
|
||||
currentGlowColor.copy(
|
||||
alpha = 0.8f * animatedGlowAlpha.value,
|
||||
),
|
||||
currentGlowColor.copy(
|
||||
alpha = 0.4f * animatedGlowAlpha.value,
|
||||
),
|
||||
Color.Transparent,
|
||||
),
|
||||
center = center,
|
||||
radius = glowRadius,
|
||||
),
|
||||
radius = glowRadius,
|
||||
blendMode = BlendMode.Screen,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(modifier = iconModifier) { TopLevelNavIcon(destination, connectionState) }
|
||||
}
|
||||
},
|
||||
selected = isSelected,
|
||||
|
|
@ -309,6 +386,25 @@ fun MainScreen(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopLevelNavIcon(destination: TopLevelDestination, connectionState: MeshService.ConnectionState) {
|
||||
val iconTint =
|
||||
when {
|
||||
destination == TopLevelDestination.Connections -> connectionState.getConnectionColor()
|
||||
else -> LocalContentColor.current
|
||||
}
|
||||
Icon(
|
||||
imageVector =
|
||||
if (destination == TopLevelDestination.Connections) {
|
||||
connectionState.getConnectionIcon()
|
||||
} else {
|
||||
destination.icon
|
||||
},
|
||||
contentDescription = stringResource(id = destination.label),
|
||||
tint = iconTint,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun VersionChecks(viewModel: UIViewModel) {
|
||||
|
|
@ -322,7 +418,8 @@ private fun VersionChecks(viewModel: UIViewModel) {
|
|||
|
||||
val currentDeviceHardware by viewModel.deviceHardware.collectAsStateWithLifecycle(null)
|
||||
|
||||
val latestStableFirmwareRelease by viewModel.latestStableFirmwareRelease.collectAsState(DeviceVersion("2.6.4"))
|
||||
val latestStableFirmwareRelease by
|
||||
viewModel.latestStableFirmwareRelease.collectAsStateWithLifecycle(DeviceVersion("2.6.4"))
|
||||
LaunchedEffect(connectionState, firmwareEdition) {
|
||||
if (connectionState == MeshService.ConnectionState.CONNECTED) {
|
||||
firmwareEdition?.let { edition ->
|
||||
|
|
@ -419,7 +516,7 @@ private fun MainAppBar(
|
|||
val totalNodeCount by viewModel.totalNodeCount.collectAsStateWithLifecycle(0)
|
||||
TopAppBar(
|
||||
title = {
|
||||
val title =
|
||||
val titleText =
|
||||
when {
|
||||
currentDestination == null || currentDestination.isTopLevel() ->
|
||||
stringResource(id = R.string.app_name)
|
||||
|
|
@ -435,7 +532,7 @@ private fun MainAppBar(
|
|||
else -> stringResource(id = R.string.app_name)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
text = titleText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
|
|
@ -514,7 +611,7 @@ private fun MainMenuActions(isManaged: Boolean, onAction: (MainMenuAction) -> Un
|
|||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.background.copy(alpha = 1f)),
|
||||
modifier = Modifier.background(colorScheme.background.copy(alpha = 1f)),
|
||||
) {
|
||||
MainMenuAction.entries.forEach { action ->
|
||||
DropdownMenuItem(
|
||||
|
|
@ -552,17 +649,3 @@ private fun MeshService.ConnectionState.getTooltipString(): String = when (this)
|
|||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue