mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: implement global SnackbarManager and consolidate common UI setup (#4909)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
9b8ac6a460
commit
553ca2f8ed
38 changed files with 705 additions and 515 deletions
|
|
@ -57,7 +57,6 @@ import org.meshtastic.app.node.metrics.getTracerouteMapOverlayInsets
|
|||
import org.meshtastic.app.ui.MainScreen
|
||||
import org.meshtastic.core.barcode.rememberBarcodeScanner
|
||||
import org.meshtastic.core.common.util.toMeshtasticUri
|
||||
import org.meshtastic.core.model.util.dispatchMeshtasticUri
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.nfc.NfcScannerEffect
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -217,11 +216,9 @@ class MainActivity : ComponentActivity() {
|
|||
return
|
||||
}
|
||||
|
||||
uri.dispatchMeshtasticUri(
|
||||
onChannel = { model.setRequestChannelSet(it) },
|
||||
onContact = { model.setSharedContactRequested(it) },
|
||||
onInvalid = { lifecycleScope.launch { showToast(Res.string.channel_invalid) } },
|
||||
)
|
||||
model.handleScannedUri(uri.toMeshtasticUri()) {
|
||||
lifecycleScope.launch { showToast(Res.string.channel_invalid) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createShareIntent(message: String): PendingIntent {
|
||||
|
|
|
|||
|
|
@ -24,14 +24,12 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.recalculateWindowInsets
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
|
@ -52,7 +50,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -63,42 +60,28 @@ import androidx.navigation3.runtime.entryProvider
|
|||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.meshtastic.app.BuildConfig
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DeviceType
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.MeshtasticNavSavedStateConfig
|
||||
import org.meshtastic.core.navigation.NodeDetailRoutes
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.navigation.TopLevelDestination
|
||||
import org.meshtastic.core.navigation.navigateTopLevel
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.app_too_old
|
||||
import org.meshtastic.core.resources.connected
|
||||
import org.meshtastic.core.resources.connecting
|
||||
import org.meshtastic.core.resources.device_sleeping
|
||||
import org.meshtastic.core.resources.disconnected
|
||||
import org.meshtastic.core.resources.firmware_old
|
||||
import org.meshtastic.core.resources.firmware_too_old
|
||||
import org.meshtastic.core.resources.must_update
|
||||
import org.meshtastic.core.resources.okay
|
||||
import org.meshtastic.core.resources.should_update
|
||||
import org.meshtastic.core.resources.should_update_firmware
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.resources.view_on_map
|
||||
import org.meshtastic.core.service.MeshService
|
||||
import org.meshtastic.core.ui.component.AlertHost
|
||||
import org.meshtastic.core.ui.component.MeshtasticCommonAppSetup
|
||||
import org.meshtastic.core.ui.component.MeshtasticSnackbarProvider
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.component.SharedDialogs
|
||||
import org.meshtastic.core.ui.navigation.icon
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
|
||||
import org.meshtastic.core.ui.util.annotateTraceroute
|
||||
import org.meshtastic.core.ui.util.toMessageRes
|
||||
import org.meshtastic.core.ui.viewmodel.UIViewModel
|
||||
import org.meshtastic.feature.connections.ScannerViewModel
|
||||
import org.meshtastic.feature.connections.navigation.connectionsGraph
|
||||
|
|
@ -117,71 +100,16 @@ fun MainScreen(uIViewModel: UIViewModel = koinViewModel(), scanModel: ScannerVie
|
|||
// LaunchedEffect(uIViewModel) { uIViewModel.navigationDeepLink.collectLatest { uri -> navController.navigate(uri) }
|
||||
// }
|
||||
val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val requestChannelSet by uIViewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
val sharedContactRequested by uIViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
val unreadMessageCount by uIViewModel.unreadMessageCount.collectAsStateWithLifecycle()
|
||||
|
||||
SharedDialogs(
|
||||
connectionState = connectionState,
|
||||
sharedContactRequested = sharedContactRequested,
|
||||
requestChannelSet = requestChannelSet,
|
||||
onDismissSharedContact = { uIViewModel.clearSharedContactRequested() },
|
||||
onDismissChannelSet = { uIViewModel.clearRequestChannelUrl() },
|
||||
MeshtasticCommonAppSetup(
|
||||
uiViewModel = uIViewModel,
|
||||
onNavigateToTracerouteMap = { destNum, requestId, logUuid ->
|
||||
backStack.add(NodeDetailRoutes.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid))
|
||||
},
|
||||
)
|
||||
|
||||
VersionChecks(uIViewModel)
|
||||
|
||||
AlertHost(uIViewModel.alertManager)
|
||||
|
||||
val traceRouteResponse by uIViewModel.tracerouteResponse.collectAsStateWithLifecycle(null)
|
||||
var dismissedTracerouteRequestId by remember { mutableStateOf<Int?>(null) }
|
||||
traceRouteResponse
|
||||
?.takeIf { it.requestId != dismissedTracerouteRequestId }
|
||||
?.let { response ->
|
||||
uIViewModel.showAlert(
|
||||
titleRes = Res.string.traceroute,
|
||||
composableMessage = {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text(
|
||||
text =
|
||||
annotateTraceroute(
|
||||
response.message,
|
||||
statusGreen = colorScheme.StatusGreen,
|
||||
statusYellow = colorScheme.StatusYellow,
|
||||
statusOrange = colorScheme.StatusOrange,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmTextRes = Res.string.view_on_map,
|
||||
onConfirm = {
|
||||
val availability =
|
||||
uIViewModel.tracerouteMapAvailability(
|
||||
forwardRoute = response.forwardRoute,
|
||||
returnRoute = response.returnRoute,
|
||||
)
|
||||
val errorRes = availability.toMessageRes()
|
||||
if (errorRes == null) {
|
||||
dismissedTracerouteRequestId = response.requestId
|
||||
backStack.add(
|
||||
NodeDetailRoutes.TracerouteMap(
|
||||
destNum = response.destinationNodeNum,
|
||||
requestId = response.requestId,
|
||||
logUuid = response.logUuid,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
uIViewModel.showAlert(titleRes = Res.string.traceroute, messageRes = errorRes)
|
||||
uIViewModel.clearTracerouteResponse()
|
||||
}
|
||||
},
|
||||
dismissTextRes = Res.string.okay,
|
||||
onDismiss = {
|
||||
uIViewModel.clearTracerouteResponse()
|
||||
dismissedTracerouteRequestId = null
|
||||
},
|
||||
)
|
||||
}
|
||||
AndroidAppVersionCheck(uIViewModel)
|
||||
val navSuiteType =
|
||||
NavigationSuiteScaffoldDefaults.navigationSuiteType(
|
||||
currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true),
|
||||
|
|
@ -280,96 +208,70 @@ fun MainScreen(uIViewModel: UIViewModel = koinViewModel(), scanModel: ScannerVie
|
|||
TopLevelDestination.Nodes -> {
|
||||
val onNodesList = currentKey is NodesRoutes.Nodes
|
||||
if (!onNodesList) {
|
||||
if (backStack.isNotEmpty()) {
|
||||
backStack[0] = destination.route
|
||||
while (backStack.size > 1) backStack.removeAt(backStack.lastIndex)
|
||||
} else {
|
||||
backStack.add(destination.route)
|
||||
}
|
||||
backStack.navigateTopLevel(destination.route)
|
||||
}
|
||||
uIViewModel.emitScrollToTopEvent(ScrollToTopEvent.NodesTabPressed)
|
||||
}
|
||||
TopLevelDestination.Conversations -> {
|
||||
val onConversationsList = currentKey is ContactsRoutes.Contacts
|
||||
if (!onConversationsList) {
|
||||
if (backStack.isNotEmpty()) {
|
||||
backStack[0] = destination.route
|
||||
while (backStack.size > 1) backStack.removeAt(backStack.lastIndex)
|
||||
} else {
|
||||
backStack.add(destination.route)
|
||||
}
|
||||
backStack.navigateTopLevel(destination.route)
|
||||
}
|
||||
uIViewModel.emitScrollToTopEvent(ScrollToTopEvent.ConversationsTabPressed)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
} else {
|
||||
if (backStack.isNotEmpty()) {
|
||||
backStack[0] = destination.route
|
||||
while (backStack.size > 1) backStack.removeAt(backStack.lastIndex)
|
||||
} else {
|
||||
backStack.add(destination.route)
|
||||
}
|
||||
backStack.navigateTopLevel(destination.route)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
val provider =
|
||||
entryProvider<NavKey> {
|
||||
contactsGraph(backStack, uIViewModel.scrollToTopEventFlow)
|
||||
nodesGraph(
|
||||
backStack = backStack,
|
||||
scrollToTopEvents = uIViewModel.scrollToTopEventFlow,
|
||||
nodeMapScreen = { destNum, onNavigateUp ->
|
||||
val vm =
|
||||
org.koin.compose.viewmodel.koinViewModel<org.meshtastic.feature.map.node.NodeMapViewModel>()
|
||||
vm.setDestNum(destNum)
|
||||
org.meshtastic.app.map.node.NodeMapScreen(vm, onNavigateUp = onNavigateUp)
|
||||
},
|
||||
)
|
||||
mapGraph(backStack)
|
||||
channelsGraph(backStack)
|
||||
connectionsGraph(backStack)
|
||||
settingsGraph(backStack)
|
||||
firmwareGraph(backStack)
|
||||
}
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
entryProvider = provider,
|
||||
modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding(),
|
||||
)
|
||||
MeshtasticSnackbarProvider(
|
||||
snackbarManager = uIViewModel.snackbarManager,
|
||||
hostModifier = Modifier.safeDrawingPadding().padding(bottom = 16.dp),
|
||||
) {
|
||||
val provider =
|
||||
entryProvider<NavKey> {
|
||||
contactsGraph(backStack, uIViewModel.scrollToTopEventFlow)
|
||||
nodesGraph(
|
||||
backStack = backStack,
|
||||
scrollToTopEvents = uIViewModel.scrollToTopEventFlow,
|
||||
nodeMapScreen = { destNum, onNavigateUp ->
|
||||
val vm =
|
||||
org.koin.compose.viewmodel.koinViewModel<
|
||||
org.meshtastic.feature.map.node.NodeMapViewModel,
|
||||
>()
|
||||
vm.setDestNum(destNum)
|
||||
org.meshtastic.app.map.node.NodeMapScreen(vm, onNavigateUp = onNavigateUp)
|
||||
},
|
||||
)
|
||||
mapGraph(backStack)
|
||||
channelsGraph(backStack)
|
||||
connectionsGraph(backStack)
|
||||
settingsGraph(backStack)
|
||||
firmwareGraph(backStack)
|
||||
}
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
entryProvider = provider,
|
||||
modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun VersionChecks(viewModel: UIViewModel) {
|
||||
private fun AndroidAppVersionCheck(viewModel: UIViewModel) {
|
||||
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val myNodeInfo by viewModel.myNodeInfo.collectAsStateWithLifecycle()
|
||||
|
||||
val myFirmwareVersion = myNodeInfo?.firmwareVersion
|
||||
|
||||
val firmwareEdition by viewModel.firmwareEdition.collectAsStateWithLifecycle(null)
|
||||
|
||||
val latestStableFirmwareRelease by
|
||||
viewModel.latestStableFirmwareRelease.collectAsStateWithLifecycle(DeviceVersion("2.6.4"))
|
||||
LaunchedEffect(connectionState, firmwareEdition) {
|
||||
if (connectionState == ConnectionState.Connected) {
|
||||
firmwareEdition?.let { edition -> Logger.d { "FirmwareEdition: ${edition.name}" } }
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the device is running an old app version or firmware version
|
||||
// Check if the device is running an old app version
|
||||
LaunchedEffect(connectionState, myNodeInfo) {
|
||||
if (connectionState == ConnectionState.Connected) {
|
||||
Logger.i {
|
||||
"[FW_CHECK] Connection state: $connectionState, " +
|
||||
"myNodeInfo: ${if (myNodeInfo != null) "present" else "null"}, " +
|
||||
"firmwareVersion: ${myFirmwareVersion ?: "null"}"
|
||||
}
|
||||
|
||||
myNodeInfo?.let { info ->
|
||||
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE && BuildConfig.DEBUG.not()
|
||||
Logger.d {
|
||||
|
|
@ -384,49 +286,8 @@ private fun VersionChecks(viewModel: UIViewModel) {
|
|||
messageRes = Res.string.must_update,
|
||||
onConfirm = { viewModel.setDeviceAddress("n") },
|
||||
)
|
||||
} else {
|
||||
myFirmwareVersion
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { fwVersion ->
|
||||
val curVer = DeviceVersion(fwVersion)
|
||||
Logger.i {
|
||||
"[FW_CHECK] Firmware version comparison - " +
|
||||
"device: $curVer (raw: $fwVersion), " +
|
||||
"absoluteMin: ${MeshService.absoluteMinDeviceVersion}, " +
|
||||
"min: ${MeshService.minDeviceVersion}"
|
||||
}
|
||||
|
||||
if (curVer < MeshService.absoluteMinDeviceVersion) {
|
||||
Logger.w {
|
||||
"[FW_CHECK] Firmware too old - " +
|
||||
"device: $curVer < absoluteMin: ${MeshService.absoluteMinDeviceVersion}"
|
||||
}
|
||||
val title = getString(Res.string.firmware_too_old)
|
||||
val message = getString(Res.string.firmware_old)
|
||||
viewModel.showAlert(
|
||||
title = title,
|
||||
html = message,
|
||||
onConfirm = { viewModel.setDeviceAddress("n") },
|
||||
)
|
||||
} else if (curVer < MeshService.minDeviceVersion) {
|
||||
Logger.w {
|
||||
"[FW_CHECK] Firmware should update - " +
|
||||
"device: $curVer < min: ${MeshService.minDeviceVersion}"
|
||||
}
|
||||
val title = getString(Res.string.should_update_firmware)
|
||||
val message = getString(Res.string.should_update, latestStableFirmwareRelease.asString)
|
||||
viewModel.showAlert(title = title, message = message, onConfirm = {})
|
||||
} else {
|
||||
Logger.i { "[FW_CHECK] Firmware version OK - device: $curVer meets requirements" }
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
Logger.w { "[FW_CHECK] Firmware version is null or blank despite myNodeInfo being present" }
|
||||
}
|
||||
}
|
||||
} ?: run { Logger.d { "[FW_CHECK] myNodeInfo is null, skipping firmware check" } }
|
||||
} else {
|
||||
Logger.d { "[FW_CHECK] Not connected (state: $connectionState), skipping firmware check" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue