diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt index bc326b428..80f1cb43c 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/SettingsNavigation.kt @@ -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): AndroidRadioConfigViewModel { + val viewModel = koinViewModel() + 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.settingsGraph(backStack: NavBackStack) { entry { SettingsScreen( settingsViewModel = koinViewModel(), - viewModel = koinViewModel(), + viewModel = getRadioConfigViewModel(backStack), onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) }, ) { backStack.add(it) @@ -87,7 +102,7 @@ fun EntryProviderScope.settingsGraph(backStack: NavBackStack) { entry { SettingsScreen( settingsViewModel = koinViewModel(), - viewModel = koinViewModel(), + viewModel = getRadioConfigViewModel(backStack), onClickNodeChip = { backStack.add(NodesRoutes.NodeDetailGraph(it)) }, ) { backStack.add(it) @@ -96,7 +111,7 @@ fun EntryProviderScope.settingsGraph(backStack: NavBackStack) { entry { DeviceConfigurationScreen( - viewModel = koinViewModel(), + viewModel = getRadioConfigViewModel(backStack), onBack = { backStack.removeLastOrNull() }, onNavigate = { route -> backStack.add(route) }, ) @@ -106,7 +121,7 @@ fun EntryProviderScope.settingsGraph(backStack: NavBackStack) { val settingsViewModel: AndroidSettingsViewModel = koinViewModel() val excludedModulesUnlocked by settingsViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle() ModuleConfigurationScreen( - viewModel = koinViewModel(), + viewModel = getRadioConfigViewModel(backStack), excludedModulesUnlocked = excludedModulesUnlocked, onBack = { backStack.removeLastOrNull() }, onNavigate = { route -> backStack.add(route) }, @@ -114,10 +129,7 @@ fun EntryProviderScope.settingsGraph(backStack: NavBackStack) { } entry { - AdministrationScreen( - viewModel = koinViewModel(), - onBack = { backStack.removeLastOrNull() }, - ) + AdministrationScreen(viewModel = getRadioConfigViewModel(backStack), onBack = { backStack.removeLastOrNull() }) } entry { @@ -126,7 +138,7 @@ fun EntryProviderScope.settingsGraph(backStack: NavBackStack) { } 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.settingsGraph(backStack: NavBackStack) { } 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.settingsGraph(backStack: NavBackStack) { fun EntryProviderScope.configComposable( route: KClass, + backStack: NavBackStack, content: @Composable (AndroidRadioConfigViewModel) -> Unit, ) { - addEntryProvider(route) { content(koinViewModel()) } + addEntryProvider(route) { content(getRadioConfigViewModel(backStack)) } } inline fun EntryProviderScope.configComposable( + backStack: NavBackStack, noinline content: @Composable (AndroidRadioConfigViewModel) -> Unit, ) { - entry { content(koinViewModel()) } + entry { content(getRadioConfigViewModel(backStack)) } } diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 30f82abb4..ca380577d 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -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 } -} \ No newline at end of file +} diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt index d274ebd69..46e6fdb4c 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt @@ -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): RadioConfigViewModel { + val viewModel = koinViewModel() + 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.desktopSettingsGraph(backStack: NavBackStack { DesktopSettingsScreen( - radioConfigViewModel = koinViewModel(), + radioConfigViewModel = getRadioConfigViewModel(backStack), settingsViewModel = koinViewModel(), onNavigate = { route -> backStack.add(route) }, ) @@ -87,7 +101,7 @@ fun EntryProviderScope.desktopSettingsGraph(backStack: NavBackStack { DesktopSettingsScreen( - radioConfigViewModel = koinViewModel(), + radioConfigViewModel = getRadioConfigViewModel(backStack), settingsViewModel = koinViewModel(), onNavigate = { route -> backStack.add(route) }, ) @@ -96,7 +110,7 @@ fun EntryProviderScope.desktopSettingsGraph(backStack: NavBackStack { DeviceConfigurationScreen( - viewModel = koinViewModel(), + viewModel = getRadioConfigViewModel(backStack), onBack = { backStack.removeLastOrNull() }, onNavigate = { route -> backStack.add(route) }, ) @@ -107,7 +121,7 @@ fun EntryProviderScope.desktopSettingsGraph(backStack: NavBackStack(), + viewModel = getRadioConfigViewModel(backStack), excludedModulesUnlocked = excludedModulesUnlocked, onBack = { backStack.removeLastOrNull() }, onNavigate = { route -> backStack.add(route) }, @@ -116,10 +130,7 @@ fun EntryProviderScope.desktopSettingsGraph(backStack: NavBackStack { - AdministrationScreen( - viewModel = koinViewModel(), - onBack = { backStack.removeLastOrNull() }, - ) + AdministrationScreen(viewModel = getRadioConfigViewModel(backStack), onBack = { backStack.removeLastOrNull() }) } // Clean node database — shared commonMain composable @@ -139,7 +150,7 @@ fun EntryProviderScope.desktopSettingsGraph(backStack: NavBackStack - 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.desktopSettingsGraph(backStack: NavBackStack - 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.desktopSettingsGraph(backStack: NavBackStack EntryProviderScope.desktopConfigComposable( route: KClass, + backStack: NavBackStack, content: @Composable (RadioConfigViewModel) -> Unit, ) { - addEntryProvider(route) { content(koinViewModel()) } + addEntryProvider(route) { content(getRadioConfigViewModel(backStack)) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index 793499d70..5d7c5951b 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -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("destNum") + private val destNumFlow = MutableStateFlow(savedStateHandle.get("destNum")) + + fun initDestNum(id: Int?) { + if (id != null && destNumFlow.value != id) { + destNumFlow.value = id + } + } private val _destNode = MutableStateFlow(null) val destNode: StateFlow @@ -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" }