mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-27 10:11:48 -04:00
refactor(settings): improve destination node handling in RadioConfigViewModel (#4790)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
@@ -72,12 +72,27 @@ import org.meshtastic.feature.settings.radio.component.TrafficManagementConfigSc
|
||||
import org.meshtastic.feature.settings.radio.component.UserConfigScreen
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@PublishedApi
|
||||
@Composable
|
||||
internal fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): AndroidRadioConfigViewModel {
|
||||
val viewModel = koinViewModel<AndroidRadioConfigViewModel>()
|
||||
LaunchedEffect(backStack) {
|
||||
val destNum =
|
||||
backStack.lastOrNull { it is SettingsRoutes.Settings }?.let { (it as SettingsRoutes.Settings).destNum }
|
||||
?: backStack
|
||||
.lastOrNull { it is SettingsRoutes.SettingsGraph }
|
||||
?.let { (it as SettingsRoutes.SettingsGraph).destNum }
|
||||
viewModel.initDestNum(destNum)
|
||||
}
|
||||
return viewModel
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||
entry<SettingsRoutes.SettingsGraph> {
|
||||
SettingsScreen(
|
||||
settingsViewModel = koinViewModel<AndroidSettingsViewModel>(),
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
viewModel = getRadioConfigViewModel(backStack),
|
||||
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
||||
) {
|
||||
backStack.add(it)
|
||||
@@ -87,7 +102,7 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||
entry<SettingsRoutes.Settings> {
|
||||
SettingsScreen(
|
||||
settingsViewModel = koinViewModel<AndroidSettingsViewModel>(),
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
viewModel = getRadioConfigViewModel(backStack),
|
||||
onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
|
||||
) {
|
||||
backStack.add(it)
|
||||
@@ -96,7 +111,7 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||
|
||||
entry<SettingsRoutes.DeviceConfiguration> {
|
||||
DeviceConfigurationScreen(
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
viewModel = getRadioConfigViewModel(backStack),
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
onNavigate = { route -> backStack.add(route) },
|
||||
)
|
||||
@@ -106,7 +121,7 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||
val settingsViewModel: AndroidSettingsViewModel = koinViewModel()
|
||||
val excludedModulesUnlocked by settingsViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
|
||||
ModuleConfigurationScreen(
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
viewModel = getRadioConfigViewModel(backStack),
|
||||
excludedModulesUnlocked = excludedModulesUnlocked,
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
onNavigate = { route -> backStack.add(route) },
|
||||
@@ -114,10 +129,7 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||
}
|
||||
|
||||
entry<SettingsRoutes.Administration> {
|
||||
AdministrationScreen(
|
||||
viewModel = koinViewModel<AndroidRadioConfigViewModel>(),
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
)
|
||||
AdministrationScreen(viewModel = getRadioConfigViewModel(backStack), onBack = { backStack.removeLastOrNull() })
|
||||
}
|
||||
|
||||
entry<SettingsRoutes.CleanNodeDb> {
|
||||
@@ -126,7 +138,7 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||
}
|
||||
|
||||
ConfigRoute.entries.forEach { routeInfo ->
|
||||
configComposable(routeInfo.route::class) { viewModel ->
|
||||
configComposable(routeInfo.route::class, backStack) { viewModel ->
|
||||
LaunchedEffect(Unit) { viewModel.setResponseStateLoading(routeInfo) }
|
||||
when (routeInfo) {
|
||||
ConfigRoute.USER -> UserConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
@@ -144,7 +156,7 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||
}
|
||||
|
||||
ModuleRoute.entries.forEach { routeInfo ->
|
||||
configComposable(routeInfo.route::class) { viewModel ->
|
||||
configComposable(routeInfo.route::class, backStack) { viewModel ->
|
||||
LaunchedEffect(Unit) { viewModel.setResponseStateLoading(routeInfo) }
|
||||
when (routeInfo) {
|
||||
ModuleRoute.MQTT -> MQTTConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
@@ -196,13 +208,15 @@ fun EntryProviderScope<NavKey>.settingsGraph(backStack: NavBackStack<NavKey>) {
|
||||
|
||||
fun <R : Route> EntryProviderScope<NavKey>.configComposable(
|
||||
route: KClass<R>,
|
||||
backStack: NavBackStack<NavKey>,
|
||||
content: @Composable (AndroidRadioConfigViewModel) -> Unit,
|
||||
) {
|
||||
addEntryProvider(route) { content(koinViewModel<AndroidRadioConfigViewModel>()) }
|
||||
addEntryProvider(route) { content(getRadioConfigViewModel(backStack)) }
|
||||
}
|
||||
|
||||
inline fun <reified R : Route> EntryProviderScope<NavKey>.configComposable(
|
||||
backStack: NavBackStack<NavKey>,
|
||||
noinline content: @Composable (AndroidRadioConfigViewModel) -> Unit,
|
||||
) {
|
||||
entry<R> { content(koinViewModel<AndroidRadioConfigViewModel>()) }
|
||||
entry<R> { content(getRadioConfigViewModel(backStack)) }
|
||||
}
|
||||
|
||||
@@ -52,32 +52,26 @@ compose.desktop {
|
||||
}
|
||||
|
||||
nativeDistributions {
|
||||
targetFormats(
|
||||
TargetFormat.Dmg,
|
||||
TargetFormat.Exe,
|
||||
TargetFormat.Msi,
|
||||
TargetFormat.Deb,
|
||||
TargetFormat.Rpm,
|
||||
)
|
||||
targetFormats(TargetFormat.Dmg, TargetFormat.Exe, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm)
|
||||
packageName = "Meshtastic"
|
||||
|
||||
// Ensure critical JVM modules are included in the custom JRE bundled with the app.
|
||||
// jdeps might miss some of these if they are loaded via reflection or JNI.
|
||||
modules(
|
||||
"java.net.http", // Ktor Java client
|
||||
"jdk.crypto.ec", // Required for SSL/TLS HTTPS requests
|
||||
"jdk.unsupported", // sun.misc.Unsafe used by Coroutines & Okio
|
||||
"java.sql", // Sometimes required by SQLite JNI
|
||||
"java.naming" // Required by Ktor for DNS resolution
|
||||
"java.net.http", // Ktor Java client
|
||||
"jdk.crypto.ec", // Required for SSL/TLS HTTPS requests
|
||||
"jdk.unsupported", // sun.misc.Unsafe used by Coroutines & Okio
|
||||
"java.sql", // Sometimes required by SQLite JNI
|
||||
"java.naming", // Required by Ktor for DNS resolution
|
||||
)
|
||||
|
||||
|
||||
// Default JVM arguments for the packaged application
|
||||
// Increase max heap size to prevent OOM issues on complex maps/data
|
||||
jvmArgs("-Xmx2G")
|
||||
|
||||
// App Icon & OS Specific Configurations
|
||||
macOS {
|
||||
iconFile.set(project.file("src/main/resources/icon.icns"))
|
||||
macOS {
|
||||
iconFile.set(project.file("src/main/resources/icon.icns"))
|
||||
// TODO: To prepare for real distribution on macOS, you'll need to sign and notarize.
|
||||
// You can inject these from CI environment variables.
|
||||
// bundleID = "org.meshtastic.desktop"
|
||||
@@ -86,22 +80,23 @@ compose.desktop {
|
||||
// appleID = System.getenv("APPLE_ID")
|
||||
// appStorePassword = System.getenv("APPLE_APP_SPECIFIC_PASSWORD")
|
||||
}
|
||||
windows {
|
||||
iconFile.set(project.file("src/main/resources/icon.ico"))
|
||||
windows {
|
||||
iconFile.set(project.file("src/main/resources/icon.ico"))
|
||||
menuGroup = "Meshtastic"
|
||||
// TODO: Must generate and set a consistent UUID for Windows upgrades.
|
||||
// TODO: Must generate and set a consistent UUID for Windows upgrades.
|
||||
// upgradeUuid = "YOUR-UPGRADE-UUID-HERE"
|
||||
}
|
||||
linux {
|
||||
iconFile.set(project.file("src/main/resources/icon.png"))
|
||||
linux {
|
||||
iconFile.set(project.file("src/main/resources/icon.png"))
|
||||
menuGroup = "Network"
|
||||
}
|
||||
|
||||
// Read version from project properties (passed by CI) or default to 1.0.0
|
||||
// Native installers require strict numeric semantic versions (X.Y.Z) without suffixes
|
||||
val rawVersion = project.findProperty("android.injected.version.name")?.toString()
|
||||
?: System.getenv("VERSION_NAME")
|
||||
?: "1.0.0"
|
||||
val rawVersion =
|
||||
project.findProperty("android.injected.version.name")?.toString()
|
||||
?: System.getenv("VERSION_NAME")
|
||||
?: "1.0.0"
|
||||
val sanitizedVersion = Regex("^\\d+\\.\\d+\\.\\d+").find(rawVersion)?.value ?: "1.0.0"
|
||||
packageVersion = sanitizedVersion
|
||||
|
||||
@@ -207,4 +202,4 @@ aboutLibraries {
|
||||
duplicationMode = DuplicateMode.MERGE
|
||||
duplicationRule = DuplicateRule.SIMPLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,20 @@ import org.meshtastic.feature.settings.radio.component.TrafficManagementConfigSc
|
||||
import org.meshtastic.feature.settings.radio.component.UserConfigScreen
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Composable
|
||||
private fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): RadioConfigViewModel {
|
||||
val viewModel = koinViewModel<RadioConfigViewModel>()
|
||||
LaunchedEffect(backStack) {
|
||||
val destNum =
|
||||
backStack.lastOrNull { it is SettingsRoutes.Settings }?.let { (it as SettingsRoutes.Settings).destNum }
|
||||
?: backStack
|
||||
.lastOrNull { it is SettingsRoutes.SettingsGraph }
|
||||
?.let { (it as SettingsRoutes.SettingsGraph).destNum }
|
||||
viewModel.initDestNum(destNum)
|
||||
}
|
||||
return viewModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers real settings feature composables into the desktop navigation graph.
|
||||
*
|
||||
@@ -79,7 +93,7 @@ fun EntryProviderScope<NavKey>.desktopSettingsGraph(backStack: NavBackStack<NavK
|
||||
// Top-level settings — desktop-specific screen (Android version uses Activity, permissions, etc.)
|
||||
entry<SettingsRoutes.SettingsGraph> {
|
||||
DesktopSettingsScreen(
|
||||
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
|
||||
radioConfigViewModel = getRadioConfigViewModel(backStack),
|
||||
settingsViewModel = koinViewModel<SettingsViewModel>(),
|
||||
onNavigate = { route -> backStack.add(route) },
|
||||
)
|
||||
@@ -87,7 +101,7 @@ fun EntryProviderScope<NavKey>.desktopSettingsGraph(backStack: NavBackStack<NavK
|
||||
|
||||
entry<SettingsRoutes.Settings> {
|
||||
DesktopSettingsScreen(
|
||||
radioConfigViewModel = koinViewModel<RadioConfigViewModel>(),
|
||||
radioConfigViewModel = getRadioConfigViewModel(backStack),
|
||||
settingsViewModel = koinViewModel<SettingsViewModel>(),
|
||||
onNavigate = { route -> backStack.add(route) },
|
||||
)
|
||||
@@ -96,7 +110,7 @@ fun EntryProviderScope<NavKey>.desktopSettingsGraph(backStack: NavBackStack<NavK
|
||||
// Device configuration — shared commonMain composable
|
||||
entry<SettingsRoutes.DeviceConfiguration> {
|
||||
DeviceConfigurationScreen(
|
||||
viewModel = koinViewModel<RadioConfigViewModel>(),
|
||||
viewModel = getRadioConfigViewModel(backStack),
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
onNavigate = { route -> backStack.add(route) },
|
||||
)
|
||||
@@ -107,7 +121,7 @@ fun EntryProviderScope<NavKey>.desktopSettingsGraph(backStack: NavBackStack<NavK
|
||||
val settingsViewModel: SettingsViewModel = koinViewModel()
|
||||
val excludedModulesUnlocked by settingsViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
|
||||
ModuleConfigurationScreen(
|
||||
viewModel = koinViewModel<RadioConfigViewModel>(),
|
||||
viewModel = getRadioConfigViewModel(backStack),
|
||||
excludedModulesUnlocked = excludedModulesUnlocked,
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
onNavigate = { route -> backStack.add(route) },
|
||||
@@ -116,10 +130,7 @@ fun EntryProviderScope<NavKey>.desktopSettingsGraph(backStack: NavBackStack<NavK
|
||||
|
||||
// Administration — shared commonMain composable
|
||||
entry<SettingsRoutes.Administration> {
|
||||
AdministrationScreen(
|
||||
viewModel = koinViewModel<RadioConfigViewModel>(),
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
)
|
||||
AdministrationScreen(viewModel = getRadioConfigViewModel(backStack), onBack = { backStack.removeLastOrNull() })
|
||||
}
|
||||
|
||||
// Clean node database — shared commonMain composable
|
||||
@@ -139,7 +150,7 @@ fun EntryProviderScope<NavKey>.desktopSettingsGraph(backStack: NavBackStack<NavK
|
||||
|
||||
// Config routes — all from commonMain composables
|
||||
ConfigRoute.entries.forEach { routeInfo ->
|
||||
desktopConfigComposable(routeInfo.route::class) { viewModel ->
|
||||
desktopConfigComposable(routeInfo.route::class, backStack) { viewModel ->
|
||||
LaunchedEffect(Unit) { viewModel.setResponseStateLoading(routeInfo) }
|
||||
when (routeInfo) {
|
||||
ConfigRoute.USER -> UserConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
@@ -160,7 +171,7 @@ fun EntryProviderScope<NavKey>.desktopSettingsGraph(backStack: NavBackStack<NavK
|
||||
|
||||
// Module routes — all from commonMain composables
|
||||
ModuleRoute.entries.forEach { routeInfo ->
|
||||
desktopConfigComposable(routeInfo.route::class) { viewModel ->
|
||||
desktopConfigComposable(routeInfo.route::class, backStack) { viewModel ->
|
||||
LaunchedEffect(Unit) { viewModel.setResponseStateLoading(routeInfo) }
|
||||
when (routeInfo) {
|
||||
ModuleRoute.MQTT -> MQTTConfigScreen(viewModel, onBack = { backStack.removeLastOrNull() })
|
||||
@@ -210,7 +221,8 @@ fun EntryProviderScope<NavKey>.desktopSettingsGraph(backStack: NavBackStack<NavK
|
||||
/** Helper to register a config/module route entry with a [RadioConfigViewModel] scoped to that entry. */
|
||||
fun <R : Route> EntryProviderScope<NavKey>.desktopConfigComposable(
|
||||
route: KClass<R>,
|
||||
backStack: NavBackStack<NavKey>,
|
||||
content: @Composable (RadioConfigViewModel) -> Unit,
|
||||
) {
|
||||
addEntryProvider(route) { content(koinViewModel<RadioConfigViewModel>()) }
|
||||
addEntryProvider(route) { content(getRadioConfigViewModel(backStack)) }
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -127,7 +126,13 @@ open class RadioConfigViewModel(
|
||||
toggleHomoglyphEncodingUseCase()
|
||||
}
|
||||
|
||||
private val destNum = savedStateHandle.get<Int>("destNum")
|
||||
private val destNumFlow = MutableStateFlow(savedStateHandle.get<Int>("destNum"))
|
||||
|
||||
fun initDestNum(id: Int?) {
|
||||
if (id != null && destNumFlow.value != id) {
|
||||
destNumFlow.value = id
|
||||
}
|
||||
}
|
||||
|
||||
private val _destNode = MutableStateFlow<Node?>(null)
|
||||
val destNode: StateFlow<Node?>
|
||||
@@ -148,8 +153,7 @@ open class RadioConfigViewModel(
|
||||
open suspend fun getCurrentLocation(): Any? = null
|
||||
|
||||
init {
|
||||
nodeRepository.nodeDBbyNum
|
||||
.mapLatest { nodes -> nodes[destNum] ?: nodes.values.firstOrNull() }
|
||||
combine(destNumFlow, nodeRepository.nodeDBbyNum) { id, nodes -> nodes[id] ?: nodes.values.firstOrNull() }
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
_destNode.value = it
|
||||
@@ -182,10 +186,9 @@ open class RadioConfigViewModel(
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
nodeRepository.myNodeInfo
|
||||
.onEach { ni ->
|
||||
_radioConfigState.update { it.copy(isLocal = (destNum == null) || (destNum == ni?.myNodeNum)) }
|
||||
}
|
||||
combine(nodeRepository.myNodeInfo, destNumFlow) { ni, id ->
|
||||
_radioConfigState.update { it.copy(isLocal = (id == null) || (id == ni?.myNodeNum)) }
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
Logger.d { "RadioConfigViewModel created" }
|
||||
|
||||
Reference in New Issue
Block a user