mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: migrate from Hilt to Koin and expand KMP common modules (#4746)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
a5390a80e7
commit
875cf1cff2
440 changed files with 3738 additions and 3508 deletions
|
|
@ -37,7 +37,6 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.Node
|
||||
|
|
@ -58,7 +57,7 @@ import org.meshtastic.feature.settings.radio.component.ShutdownConfirmationDialo
|
|||
import org.meshtastic.feature.settings.radio.component.WarningDialog
|
||||
|
||||
@Composable
|
||||
fun AdministrationScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun AdministrationScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
val enabled = state.connected && !state.responseState.isWaiting()
|
||||
|
|
@ -26,7 +26,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.navigation.Route
|
||||
|
|
@ -40,11 +39,7 @@ import org.meshtastic.feature.settings.navigation.ConfigRoute
|
|||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
|
||||
@Composable
|
||||
fun DeviceConfigurationScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
onBack: () -> Unit,
|
||||
onNavigate: (Route) -> Unit,
|
||||
) {
|
||||
fun DeviceConfigurationScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onNavigate: (Route) -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
|
||||
|
|
@ -27,7 +27,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.navigation.Route
|
||||
|
|
@ -42,8 +41,8 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
|
||||
@Composable
|
||||
fun ModuleConfigurationScreen(
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
excludedModulesUnlocked: Boolean = false,
|
||||
viewModel: RadioConfigViewModel,
|
||||
excludedModulesUnlocked: Boolean,
|
||||
onBack: () -> Unit,
|
||||
onNavigate: (Route) -> Unit,
|
||||
) {
|
||||
|
|
@ -74,7 +74,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -125,7 +124,7 @@ private var redactedKeys: List<String> = listOf("session_passkey", "private_key"
|
|||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel = hiltViewModel()) {
|
||||
fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel) {
|
||||
val listState = rememberLazyListState()
|
||||
val logs by viewModel.meshLog.collectAsStateWithLifecycle()
|
||||
val searchState by viewModel.searchState.collectAsStateWithLifecycle()
|
||||
|
|
@ -194,7 +193,8 @@ fun DebugScreen(onNavigateUp: () -> Unit, viewModel: DebugViewModel = hiltViewMo
|
|||
targetValue = if (!listState.isScrollInProgress) 1.0f else 0f,
|
||||
label = "alpha",
|
||||
)
|
||||
DebugSearchStateviewModelDefaults(
|
||||
DebugSearchStateWithViewModel(
|
||||
viewModel = viewModel,
|
||||
modifier = Modifier.graphicsLayer(alpha = animatedAlpha),
|
||||
searchState = searchState,
|
||||
filterTexts = filterTexts,
|
||||
|
|
@ -50,7 +50,6 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.debug_default_search
|
||||
|
|
@ -208,7 +207,8 @@ fun DebugSearchState(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun DebugSearchStateviewModelDefaults(
|
||||
fun DebugSearchStateWithViewModel(
|
||||
viewModel: DebugViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
searchState: SearchState,
|
||||
filterTexts: List<String>,
|
||||
|
|
@ -218,7 +218,6 @@ fun DebugSearchStateviewModelDefaults(
|
|||
onFilterModeChange: (FilterMode) -> Unit,
|
||||
onExportLogs: (() -> Unit)? = null,
|
||||
) {
|
||||
val viewModel: DebugViewModel = hiltViewModel()
|
||||
DebugSearchState(
|
||||
modifier = modifier,
|
||||
searchState = searchState,
|
||||
|
|
@ -45,7 +45,6 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -63,7 +62,7 @@ import org.meshtastic.core.resources.filter_words_summary
|
|||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
|
||||
@Composable
|
||||
fun FilterSettingsScreen(viewModel: FilterSettingsViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun FilterSettingsScreen(viewModel: FilterSettingsViewModel, onBack: () -> Unit) {
|
||||
val filterEnabled by viewModel.filterEnabled.collectAsStateWithLifecycle()
|
||||
val filterWords by viewModel.filterWords.collectAsStateWithLifecycle()
|
||||
var newWord by remember { mutableStateOf("") }
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.settings.navigation
|
||||
|
||||
import org.meshtastic.core.navigation.Route
|
||||
|
|
@ -37,7 +37,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.Node
|
||||
|
|
@ -55,7 +54,7 @@ import org.meshtastic.core.ui.component.NodeChip
|
|||
* nodes to be deleted updates automatically as filter criteria change.
|
||||
*/
|
||||
@Composable
|
||||
fun CleanNodeDatabaseScreen(viewModel: CleanNodeDatabaseViewModel = hiltViewModel()) {
|
||||
fun CleanNodeDatabaseScreen(viewModel: CleanNodeDatabaseViewModel) {
|
||||
val olderThanDays by viewModel.olderThanDays.collectAsStateWithLifecycle()
|
||||
val onlyUnknownNodes by viewModel.onlyUnknownNodes.collectAsStateWithLifecycle()
|
||||
val nodesToDelete by viewModel.nodesToDelete.collectAsStateWithLifecycle()
|
||||
|
|
@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -40,7 +39,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun AmbientLightingConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val ambientLightingConfig = state.moduleConfig.ambient_lighting ?: ModuleConfig.AmbientLightingConfig()
|
||||
val formState = rememberConfigState(initialValue = ambientLightingConfig)
|
||||
|
|
@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -43,7 +42,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val audioConfig = state.moduleConfig.audio ?: ModuleConfig.AudioConfig()
|
||||
val formState = rememberConfigState(initialValue = audioConfig)
|
||||
|
|
@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -39,7 +38,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.proto.Config
|
||||
|
||||
@Composable
|
||||
fun BluetoothConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun BluetoothConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val bluetoothConfig = state.radioConfig.bluetooth ?: Config.BluetoothConfig()
|
||||
val formState = rememberConfigState(initialValue = bluetoothConfig)
|
||||
|
|
@ -28,7 +28,6 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -54,7 +53,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val cannedMessageConfig = state.moduleConfig.canned_message ?: ModuleConfig.CannedMessageConfig()
|
||||
val messages = state.cannedMessageMessages
|
||||
|
|
@ -26,7 +26,6 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -51,7 +50,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val detectionSensorConfig = state.moduleConfig.detection_sensor ?: ModuleConfig.DetectionSensorConfig()
|
||||
val formState = rememberConfigState(initialValue = detectionSensorConfig)
|
||||
|
|
@ -58,7 +58,6 @@ import androidx.compose.ui.text.fromHtml
|
|||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import no.nordicsemi.android.common.core.registerReceiver
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
|
@ -155,7 +154,7 @@ private val Config.DeviceConfig.RebroadcastMode.description: StringResource
|
|||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun DeviceConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val deviceConfig = state.radioConfig.device ?: Config.DeviceConfig()
|
||||
val formState = rememberConfigState(initialValue = deviceConfig)
|
||||
|
|
@ -21,7 +21,6 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -58,7 +57,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
|
|||
import org.meshtastic.proto.Config
|
||||
|
||||
@Composable
|
||||
fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val displayConfig = state.radioConfig.display ?: Config.DisplayConfig()
|
||||
val formState = rememberConfigState(initialValue = displayConfig)
|
||||
|
|
@ -41,7 +41,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
|
@ -87,7 +86,7 @@ private const val MAX_RINGTONE_SIZE = 230
|
|||
fun ExternalNotificationConfigScreen(
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: RadioConfigViewModel = hiltViewModel(),
|
||||
viewModel: RadioConfigViewModel,
|
||||
) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val extNotificationConfig = state.moduleConfig.external_notification ?: ModuleConfig.ExternalNotificationConfig()
|
||||
|
|
@ -27,7 +27,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -52,7 +51,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
val destNum = destNode?.num
|
||||
|
|
@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -39,7 +38,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun NeighborInfoConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val neighborInfoConfig = state.moduleConfig.neighbor_info ?: ModuleConfig.NeighborInfoConfig()
|
||||
val formState = rememberConfigState(initialValue = neighborInfoConfig)
|
||||
|
|
@ -37,7 +37,6 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.barcode.extractWifiCredentials
|
||||
|
|
@ -91,7 +90,7 @@ private fun ScanErrorDialog(onDismiss: () -> Unit = {}) =
|
|||
MeshtasticDialog(titleRes = Res.string.error, messageRes = Res.string.wifi_qr_code_error, onDismiss = onDismiss)
|
||||
|
||||
@Composable
|
||||
fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val networkConfig = state.radioConfig.network ?: Config.NetworkConfig()
|
||||
val formState = rememberConfigState(initialValue = networkConfig)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.settings.radio.component
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -23,7 +23,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -43,7 +42,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun PaxcounterConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val paxcounterConfig = state.moduleConfig.paxcounter ?: ModuleConfig.PaxcounterConfig()
|
||||
val formState = rememberConfigState(initialValue = paxcounterConfig)
|
||||
|
|
@ -34,7 +34,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.core.location.LocationCompat
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import no.nordicsemi.android.common.permissions.ble.RequireLocation
|
||||
|
|
@ -79,7 +78,7 @@ import org.meshtastic.proto.Config
|
|||
|
||||
@Composable
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun PositionConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var phoneLocation: Location? by remember { mutableStateOf(null) }
|
||||
|
|
@ -257,7 +256,9 @@ fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
|||
enabled = state.connected && !isLocationRequiredAndDisabled,
|
||||
onClick = {
|
||||
@SuppressLint("MissingPermission")
|
||||
coroutineScope.launch { phoneLocation = viewModel.getCurrentLocation() }
|
||||
coroutineScope.launch {
|
||||
phoneLocation = viewModel.getCurrentLocation() as? android.location.Location
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(Res.string.position_config_set_fixed_from_phone))
|
||||
|
|
@ -23,7 +23,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -48,7 +47,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
|
|||
import org.meshtastic.proto.Config
|
||||
|
||||
@Composable
|
||||
fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun PowerConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val powerConfig = state.radioConfig.power ?: Config.PowerConfig()
|
||||
val formState = rememberConfigState(initialValue = powerConfig)
|
||||
|
|
@ -21,7 +21,6 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -39,7 +38,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun RangeTestConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun RangeTestConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val rangeTestConfig = state.moduleConfig.range_test ?: ModuleConfig.RangeTestConfig()
|
||||
val formState = rememberConfigState(initialValue = rangeTestConfig)
|
||||
|
|
@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -38,7 +37,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun RemoteHardwareConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val remoteHardwareConfig = state.moduleConfig.remote_hardware ?: ModuleConfig.RemoteHardwareConfig()
|
||||
val formState = rememberConfigState(initialValue = remoteHardwareConfig)
|
||||
|
|
@ -35,7 +35,6 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
|
|
@ -77,7 +76,7 @@ import java.security.SecureRandom
|
|||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun SecurityConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val node by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
val securityConfig = state.radioConfig.security ?: Config.SecurityConfig()
|
||||
|
|
@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -42,7 +41,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val serialConfig = state.moduleConfig.serial ?: ModuleConfig.SerialConfig()
|
||||
val formState = rememberConfigState(initialValue = serialConfig)
|
||||
|
|
@ -29,7 +29,6 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -42,7 +41,7 @@ import org.meshtastic.core.ui.component.TitledCard
|
|||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
|
||||
@Composable
|
||||
fun StatusMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun StatusMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val destNode by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
|
||||
|
|
@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -41,7 +40,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun StoreForwardConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val storeForwardConfig = state.moduleConfig.store_forward ?: ModuleConfig.StoreForwardConfig()
|
||||
val formState = rememberConfigState(initialValue = storeForwardConfig)
|
||||
|
|
@ -21,7 +21,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.getColorFrom
|
||||
|
|
@ -37,7 +36,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun TAKConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun TAKConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val takConfig = state.moduleConfig.tak ?: ModuleConfig.TAKConfig()
|
||||
val formState = rememberConfigState(initialValue = takConfig)
|
||||
|
|
@ -21,7 +21,6 @@ import androidx.compose.material3.HorizontalDivider
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
|
|
@ -49,7 +48,7 @@ import org.meshtastic.feature.settings.util.toDisplayString
|
|||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
@Composable
|
||||
fun TelemetryConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun TelemetryConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val telemetryConfig = state.moduleConfig.telemetry ?: ModuleConfig.TelemetryConfig()
|
||||
val formState = rememberConfigState(initialValue = telemetryConfig)
|
||||
|
|
@ -23,7 +23,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -51,7 +50,7 @@ import org.meshtastic.proto.ModuleConfig
|
|||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun TrafficManagementConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun TrafficManagementConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val tmConfig = state.moduleConfig.traffic_management ?: ModuleConfig.TrafficManagementConfig()
|
||||
val formState = rememberConfigState(initialValue = tmConfig)
|
||||
|
|
@ -26,7 +26,6 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
|
|
@ -49,7 +48,7 @@ import org.meshtastic.core.ui.component.TitledCard
|
|||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
|
||||
@Composable
|
||||
fun UserConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
fun UserConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val userConfig = state.userConfig
|
||||
val formState = rememberConfigState(initialValue = userConfig)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.settings.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -19,9 +19,7 @@ package org.meshtastic.feature.settings.util
|
|||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.fr_HT
|
||||
|
|
@ -29,7 +27,6 @@ import org.meshtastic.core.resources.preferences_system_default
|
|||
import org.meshtastic.core.resources.pt_BR
|
||||
import org.meshtastic.core.resources.zh_CN
|
||||
import org.meshtastic.core.resources.zh_TW
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.util.Locale
|
||||
|
||||
object LanguageUtils {
|
||||
|
|
@ -50,32 +47,54 @@ object LanguageUtils {
|
|||
)
|
||||
}
|
||||
|
||||
/** Using locales_config.xml, maps language tags to their localized language names (e.g.: "en" -> "English") */
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
/** Using a hardcoded list, maps language tags to their localized language names (e.g.: "en" -> "English") */
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
@Composable
|
||||
fun languageMap(): Map<String, String> {
|
||||
val resources = LocalResources.current
|
||||
val languageTags =
|
||||
remember(resources) {
|
||||
buildList {
|
||||
add(SYSTEM_DEFAULT)
|
||||
|
||||
try {
|
||||
resources.getXml(org.meshtastic.feature.settings.R.xml.locales_config).use { parser ->
|
||||
while (parser.eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.eventType == XmlPullParser.START_TAG && parser.name == "locale") {
|
||||
val languageTag =
|
||||
parser.getAttributeValue("http://schemas.android.com/apk/res/android", "name")
|
||||
languageTag?.let { add(it) }
|
||||
}
|
||||
parser.next()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e { "Error parsing locale_config.xml: ${e.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
val languageTags = remember {
|
||||
listOf(
|
||||
SYSTEM_DEFAULT,
|
||||
"en",
|
||||
"ar",
|
||||
"bg",
|
||||
"ca",
|
||||
"cs",
|
||||
"de",
|
||||
"el",
|
||||
"es",
|
||||
"et",
|
||||
"fi",
|
||||
"fr",
|
||||
"ga",
|
||||
"gl",
|
||||
"hr",
|
||||
"ht",
|
||||
"hu",
|
||||
"is",
|
||||
"it",
|
||||
"iw",
|
||||
"ja",
|
||||
"ko",
|
||||
"lt",
|
||||
"nl",
|
||||
"nb",
|
||||
"pl",
|
||||
"pt",
|
||||
"pt-BR",
|
||||
"ro",
|
||||
"ru",
|
||||
"sk",
|
||||
"sl",
|
||||
"sq",
|
||||
"sr",
|
||||
"srp",
|
||||
"sv",
|
||||
"tr",
|
||||
"uk",
|
||||
"zh-CN",
|
||||
"zh-TW",
|
||||
)
|
||||
}
|
||||
|
||||
return languageTags.associateWith { languageTag ->
|
||||
when (languageTag) {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.feature.settings.util
|
||||
|
||||
val gpioPins = (0..48).map { it to "Pin $it" }
|
||||
|
|
@ -16,12 +16,8 @@
|
|||
*/
|
||||
package org.meshtastic.feature.settings
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -30,10 +26,7 @@ import kotlinx.coroutines.flow.flowOf
|
|||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.BufferedSink
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.common.database.DatabaseManager
|
||||
import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase
|
||||
|
|
@ -53,16 +46,9 @@ import org.meshtastic.core.repository.RadioConfigRepository
|
|||
import org.meshtastic.core.repository.UiPrefs
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class SettingsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val app: android.app.Application,
|
||||
open class SettingsViewModel(
|
||||
radioConfigRepository: RadioConfigRepository,
|
||||
private val radioController: RadioController,
|
||||
private val nodeRepository: NodeRepository,
|
||||
|
|
@ -163,32 +149,15 @@ constructor(
|
|||
/**
|
||||
* Export all persisted packet data to a CSV file at the given URI.
|
||||
*
|
||||
* The CSV will include all packets, or only those matching the given port number if specified. Each row contains:
|
||||
* date, time, sender node number, sender name, sender latitude, sender longitude, receiver latitude, receiver
|
||||
* longitude, receiver elevation, received SNR, distance, hop limit, and payload.
|
||||
*
|
||||
* @param uri The destination URI for the CSV file.
|
||||
* @param filterPortnum If provided, only packets with this port number will be exported.
|
||||
*/
|
||||
@Suppress("detekt:CyclomaticComplexMethod", "detekt:LongMethod")
|
||||
fun saveDataCsv(uri: Uri, filterPortnum: Int? = null) {
|
||||
viewModelScope.launch {
|
||||
val myNodeNum = myNodeNum ?: return@launch
|
||||
writeToUri(uri) { writer -> exportDataUseCase(writer, myNodeNum, filterPortnum) }
|
||||
}
|
||||
open fun saveDataCsv(uri: Any, filterPortnum: Int? = null) {
|
||||
// To be implemented in platform-specific subclass
|
||||
}
|
||||
|
||||
private suspend inline fun writeToUri(uri: Uri, crossinline block: suspend (BufferedSink) -> Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { writer ->
|
||||
block.invoke(writer)
|
||||
}
|
||||
}
|
||||
} catch (ex: FileNotFoundException) {
|
||||
Logger.e { "Can't write file error: ${ex.message}" }
|
||||
}
|
||||
}
|
||||
protected suspend fun performDataExport(writer: BufferedSink, filterPortnum: Int?) {
|
||||
val myNodeNum = myNodeNum ?: return
|
||||
exportDataUseCase(writer, myNodeNum, filterPortnum)
|
||||
}
|
||||
}
|
||||
|
|
@ -290,8 +290,3 @@ fun DebugActiveFilters(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class FilterMode {
|
||||
OR,
|
||||
AND,
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -33,9 +32,8 @@ import kotlinx.coroutines.flow.first
|
|||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.common.util.DateFormatter
|
||||
import org.meshtastic.core.common.util.nowInstant
|
||||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.common.util.toInstant
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.model.getTracerouteResponse
|
||||
|
|
@ -62,9 +60,6 @@ import org.meshtastic.proto.StoreForwardPlusPlus
|
|||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.User
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import java.text.DateFormat
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
data class SearchMatch(val logIndex: Int, val start: Int, val end: Int, val field: String)
|
||||
|
||||
|
|
@ -75,6 +70,11 @@ data class SearchState(
|
|||
val hasMatches: Boolean = false,
|
||||
)
|
||||
|
||||
enum class FilterMode {
|
||||
AND,
|
||||
OR,
|
||||
}
|
||||
|
||||
// --- Search and Filter Managers ---
|
||||
class LogSearchManager {
|
||||
data class SearchMatch(val logIndex: Int, val start: Int, val end: Int, val field: String)
|
||||
|
|
@ -141,24 +141,24 @@ class LogSearchManager {
|
|||
return filteredLogs
|
||||
.flatMapIndexed { logIndex, log ->
|
||||
searchText.split(" ").flatMap { term ->
|
||||
val escapedTerm = Regex.escape(term)
|
||||
val escapedTerm = term // Simple regex escape or just use contains
|
||||
val regex = escapedTerm.toRegex(RegexOption.IGNORE_CASE)
|
||||
val messageMatches =
|
||||
regex.findAll(log.logMessage).map { match ->
|
||||
SearchMatch(logIndex, match.range.first, match.range.last, "message")
|
||||
regex.findAll(log.logMessage).map {
|
||||
SearchMatch(logIndex, it.range.first, it.range.last, "message")
|
||||
}
|
||||
val typeMatches =
|
||||
regex.findAll(log.messageType).map { match ->
|
||||
SearchMatch(logIndex, match.range.first, match.range.last, "type")
|
||||
regex.findAll(log.messageType).map {
|
||||
SearchMatch(logIndex, it.range.first, it.range.last, "type")
|
||||
}
|
||||
val dateMatches =
|
||||
regex.findAll(log.formattedReceivedDate).map { match ->
|
||||
SearchMatch(logIndex, match.range.first, match.range.last, "date")
|
||||
regex.findAll(log.formattedReceivedDate).map {
|
||||
SearchMatch(logIndex, it.range.first, it.range.last, "date")
|
||||
}
|
||||
val decodedPayloadMatches =
|
||||
log.decodedPayload?.let { decoded ->
|
||||
regex.findAll(decoded).map { match ->
|
||||
SearchMatch(logIndex, match.range.first, match.range.last, "decodedPayload")
|
||||
log.decodedPayload?.let {
|
||||
regex.findAll(it).map {
|
||||
SearchMatch(logIndex, it.range.first, it.range.last, "decodedPayload")
|
||||
}
|
||||
} ?: emptySequence()
|
||||
messageMatches + typeMatches + dateMatches + decodedPayloadMatches
|
||||
|
|
@ -189,35 +189,30 @@ class LogFilterManager {
|
|||
filterMode: FilterMode,
|
||||
): List<DebugViewModel.UiMeshLog> {
|
||||
if (filterTexts.isEmpty()) return logs
|
||||
return logs.filter { log ->
|
||||
return logs.filter { logItem ->
|
||||
when (filterMode) {
|
||||
FilterMode.OR ->
|
||||
filterTexts.any { filterText ->
|
||||
log.logMessage.contains(filterText, ignoreCase = true) ||
|
||||
log.messageType.contains(filterText, ignoreCase = true) ||
|
||||
log.formattedReceivedDate.contains(filterText, ignoreCase = true) ||
|
||||
(log.decodedPayload?.contains(filterText, ignoreCase = true) == true)
|
||||
filterTexts.any {
|
||||
it.contains(logItem.logMessage, ignoreCase = true) ||
|
||||
it.contains(logItem.messageType, ignoreCase = true) ||
|
||||
it.contains(logItem.formattedReceivedDate, ignoreCase = true) ||
|
||||
(logItem.decodedPayload?.contains(it, ignoreCase = true) == true)
|
||||
}
|
||||
|
||||
FilterMode.AND ->
|
||||
filterTexts.all { filterText ->
|
||||
log.logMessage.contains(filterText, ignoreCase = true) ||
|
||||
log.messageType.contains(filterText, ignoreCase = true) ||
|
||||
log.formattedReceivedDate.contains(filterText, ignoreCase = true) ||
|
||||
(log.decodedPayload?.contains(filterText, ignoreCase = true) == true)
|
||||
filterTexts.all {
|
||||
it.contains(logItem.logMessage, ignoreCase = true) ||
|
||||
it.contains(logItem.messageType, ignoreCase = true) ||
|
||||
it.contains(logItem.formattedReceivedDate, ignoreCase = true) ||
|
||||
(logItem.decodedPayload?.contains(it, ignoreCase = true) == true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val HEX_FORMAT = "%02x"
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class DebugViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
open class DebugViewModel(
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val meshLogPrefs: MeshLogPrefs,
|
||||
|
|
@ -304,13 +299,13 @@ constructor(
|
|||
}
|
||||
|
||||
private fun toUiState(databaseLogs: List<MeshLog>) = databaseLogs
|
||||
.map { log ->
|
||||
.map {
|
||||
UiMeshLog(
|
||||
uuid = log.uuid,
|
||||
messageType = log.message_type,
|
||||
formattedReceivedDate = TIME_FORMAT.format(log.received_date.toInstant().toDate()),
|
||||
logMessage = annotateMeshLogMessage(log),
|
||||
decodedPayload = decodePayloadFromMeshLog(log),
|
||||
uuid = it.uuid,
|
||||
messageType = it.message_type,
|
||||
formattedReceivedDate = DateFormatter.formatDateTime(it.received_date),
|
||||
logMessage = annotateMeshLogMessage(it),
|
||||
decodedPayload = decodePayloadFromMeshLog(it),
|
||||
)
|
||||
}
|
||||
.toImmutableList()
|
||||
|
|
@ -387,18 +382,21 @@ constructor(
|
|||
private fun StringBuilder.annotateNodeId(nodeId: Int): Boolean {
|
||||
val nodeIdStr = nodeId.toUInt().toString()
|
||||
// Only match if whitespace before and after
|
||||
val regex = Regex("""(?<=\s|^)${Regex.escape(nodeIdStr)}(?=\s|$)""")
|
||||
val regex = Regex("""(?<=\s|^)${Regex.escape(nodeIdStr)}(?=\s|$)""", RegexOption.DOT_MATCHES_ALL)
|
||||
regex.find(this)?.let { _ ->
|
||||
regex.findAll(this).toList().asReversed().forEach { match ->
|
||||
val idx = match.range.last + 1
|
||||
insert(idx, " (${nodeId.asNodeId()})")
|
||||
regex.findAll(this).toList().asReversed().forEach {
|
||||
val idx = it.range.last + 1
|
||||
insert(idx, " (${nodeId.toHex(8)})")
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun Int.asNodeId(): String = "!%08x".format(Locale.getDefault(), this)
|
||||
protected open fun Int.toHex(length: Int): String {
|
||||
// Platform specific hex implementation
|
||||
return "!$this"
|
||||
}
|
||||
|
||||
fun requestDeleteAllLogs() {
|
||||
alertManager.showAlert(
|
||||
|
|
@ -419,20 +417,16 @@ constructor(
|
|||
val decodedPayload: String? = null,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val TIME_FORMAT = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
||||
}
|
||||
|
||||
val presetFilters: List<String>
|
||||
get() = buildList {
|
||||
// Our address if available
|
||||
nodeRepository.myNodeInfo.value?.myNodeNum?.let { add("!%08x".format(it)) }
|
||||
nodeRepository.myNodeInfo.value?.myNodeNum?.let { add(it.toHex(8)) }
|
||||
// broadcast
|
||||
add("!ffffffff")
|
||||
// decoded
|
||||
add("decoded")
|
||||
// today (locale-dependent short date format)
|
||||
add(DateFormat.getDateInstance(DateFormat.SHORT).format(nowInstant.toDate()))
|
||||
add(DateFormatter.formatShortDate(nowInstant.toEpochMilliseconds()))
|
||||
// Each app name
|
||||
addAll(PortNum.entries.map { it.name })
|
||||
}
|
||||
|
|
@ -464,7 +458,7 @@ constructor(
|
|||
when (portnumValue) {
|
||||
PortNum.TEXT_MESSAGE_APP.value,
|
||||
PortNum.ALERT_APP.value,
|
||||
-> payload.toString(Charsets.UTF_8)
|
||||
-> payload.decodeToString()
|
||||
PortNum.POSITION_APP.value ->
|
||||
Position.ADAPTER.decodeOrNull(payload)?.let { Position.ADAPTER.toReadableString(it) }
|
||||
?: "Failed to decode Position"
|
||||
|
|
@ -495,17 +489,19 @@ constructor(
|
|||
} ?: "Failed to decode StoreForwardPlusPlus"
|
||||
PortNum.NEIGHBORINFO_APP.value -> decodeNeighborInfo(payload)
|
||||
PortNum.TRACEROUTE_APP.value -> decodeTraceroute(packet, payload)
|
||||
else -> payload.joinToString(" ") { HEX_FORMAT.format(it) }
|
||||
else -> payload.joinToString(" ") { it.toHex() }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
"Failed to decode payload: ${e.message}"
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun Byte.toHex(): String = this.toString()
|
||||
|
||||
private fun formatNodeWithShortName(nodeNum: Int): String {
|
||||
val user = nodeRepository.nodeDBbyNum.value[nodeNum]?.user
|
||||
val shortName = user?.short_name?.takeIf { it.isNotEmpty() } ?: ""
|
||||
val nodeId = "!%08x".format(nodeNum)
|
||||
val nodeId = nodeNum.toHex(8)
|
||||
return if (shortName.isNotEmpty()) "$nodeId ($shortName)" else nodeId
|
||||
}
|
||||
|
||||
|
|
@ -518,8 +514,8 @@ constructor(
|
|||
appendLine(" node_broadcast_interval_secs: ${info.node_broadcast_interval_secs}")
|
||||
if (info.neighbors.isNotEmpty()) {
|
||||
appendLine(" neighbors:")
|
||||
info.neighbors.forEach { n ->
|
||||
appendLine(" - node_id: ${formatNodeWithShortName(n.node_id ?: 0)} snr: ${n.snr}")
|
||||
info.neighbors.forEach {
|
||||
appendLine(" - node_id: ${formatNodeWithShortName(it.node_id ?: 0)} snr: ${it.snr}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -529,6 +525,6 @@ constructor(
|
|||
val getUsername: (Int) -> String = { nodeNum -> formatNodeWithShortName(nodeNum) }
|
||||
return packet.getTracerouteResponse(getUsername)
|
||||
?: runCatching { RouteDiscovery.ADAPTER.decode(payload).toString() }.getOrNull()
|
||||
?: payload.joinToString(" ") { HEX_FORMAT.format(it) }
|
||||
?: payload.joinToString(" ") { it.toHex() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.feature.settings.di
|
||||
|
||||
import org.koin.core.annotation.ComponentScan
|
||||
import org.koin.core.annotation.Module
|
||||
|
||||
@Module
|
||||
@ComponentScan("org.meshtastic.feature.settings")
|
||||
class FeatureSettingsModule
|
||||
|
|
@ -17,21 +17,14 @@
|
|||
package org.meshtastic.feature.settings.filter
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.meshtastic.core.repository.FilterPrefs
|
||||
import org.meshtastic.core.repository.MessageFilter
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class FilterSettingsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val filterPrefs: FilterPrefs,
|
||||
private val messageFilter: MessageFilter,
|
||||
) : ViewModel() {
|
||||
open class FilterSettingsViewModel(private val filterPrefs: FilterPrefs, private val messageFilter: MessageFilter) :
|
||||
ViewModel() {
|
||||
|
||||
private val _filterEnabled = MutableStateFlow(filterPrefs.filterEnabled.value)
|
||||
val filterEnabled: StateFlow<Boolean> = _filterEnabled.asStateFlow()
|
||||
|
|
@ -18,7 +18,6 @@ package org.meshtastic.feature.settings.radio
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -31,7 +30,6 @@ import org.meshtastic.core.resources.are_you_sure
|
|||
import org.meshtastic.core.resources.clean_node_database_confirmation
|
||||
import org.meshtastic.core.resources.clean_now
|
||||
import org.meshtastic.core.ui.util.AlertManager
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val MIN_DAYS_THRESHOLD = 7f
|
||||
|
||||
|
|
@ -39,10 +37,7 @@ private const val MIN_DAYS_THRESHOLD = 7f
|
|||
* ViewModel for [CleanNodeDatabaseScreen]. Manages the state and logic for cleaning the node database based on
|
||||
* specified criteria. The "older than X days" filter is always active.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class CleanNodeDatabaseViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
open class CleanNodeDatabaseViewModel(
|
||||
private val cleanNodeDatabaseUseCase: CleanNodeDatabaseUseCase,
|
||||
private val alertManager: AlertManager,
|
||||
) : ViewModel() {
|
||||
|
|
@ -16,34 +16,20 @@
|
|||
*/
|
||||
package org.meshtastic.feature.settings.radio
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.net.Uri
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.toRoute
|
||||
import co.touchlab.kermit.Logger
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase
|
||||
|
|
@ -87,8 +73,6 @@ import org.meshtastic.proto.LocalModuleConfig
|
|||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.User
|
||||
import java.io.FileOutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Data class that represents the current RadioConfig state. */
|
||||
data class RadioConfigState(
|
||||
|
|
@ -110,12 +94,8 @@ data class RadioConfigState(
|
|||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
class RadioConfigViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
open class RadioConfigViewModel(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val app: Application,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val packetRepository: PacketRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
|
|
@ -126,9 +106,9 @@ constructor(
|
|||
private val homoglyphEncodingPrefs: HomoglyphPrefs,
|
||||
private val toggleAnalyticsUseCase: ToggleAnalyticsUseCase,
|
||||
private val toggleHomoglyphEncodingUseCase: ToggleHomoglyphEncodingUseCase,
|
||||
private val importProfileUseCase: ImportProfileUseCase,
|
||||
private val exportProfileUseCase: ExportProfileUseCase,
|
||||
private val exportSecurityConfigUseCase: ExportSecurityConfigUseCase,
|
||||
protected val importProfileUseCase: ImportProfileUseCase,
|
||||
protected val exportProfileUseCase: ExportProfileUseCase,
|
||||
protected val exportSecurityConfigUseCase: ExportSecurityConfigUseCase,
|
||||
private val installProfileUseCase: InstallProfileUseCase,
|
||||
private val radioConfigUseCase: RadioConfigUseCase,
|
||||
private val adminActionsUseCase: AdminActionsUseCase,
|
||||
|
|
@ -166,15 +146,7 @@ constructor(
|
|||
val currentDeviceProfile
|
||||
get() = _currentDeviceProfile.value
|
||||
|
||||
@RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
suspend fun getCurrentLocation(): Location? = if (
|
||||
ContextCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
locationRepository.getLocations().firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
open suspend fun getCurrentLocation(): Any? = null
|
||||
|
||||
init {
|
||||
nodeRepository.nodeDBbyNum
|
||||
|
|
@ -254,13 +226,6 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getOwner(destNum: Int) {
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getOwner(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChannels(new: List<ChannelSettings>, old: List<ChannelSettings>) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
getChannelList(new, old).forEach { channel ->
|
||||
|
|
@ -279,13 +244,6 @@ constructor(
|
|||
_radioConfigState.update { it.copy(channelList = new) }
|
||||
}
|
||||
|
||||
private fun getChannel(destNum: Int, index: Int) {
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getChannel(destNum, index)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
|
||||
fun setConfig(config: Config) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
viewModelScope.launch {
|
||||
|
|
@ -309,13 +267,6 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getConfig(destNum: Int, configType: Int) {
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getConfig(destNum, configType)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun setModuleConfig(config: ModuleConfig) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
|
|
@ -349,76 +300,18 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getModuleConfig(destNum: Int, configType: Int) {
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getModuleConfig(destNum, configType)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
|
||||
fun setRingtone(ringtone: String) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
_radioConfigState.update { it.copy(ringtone = ringtone) }
|
||||
viewModelScope.launch { radioConfigUseCase.setRingtone(destNum, ringtone) }
|
||||
}
|
||||
|
||||
private fun getRingtone(destNum: Int) {
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getRingtone(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCannedMessages(messages: String) {
|
||||
val destNum = destNode.value?.num ?: return
|
||||
_radioConfigState.update { it.copy(cannedMessageMessages = messages) }
|
||||
viewModelScope.launch { radioConfigUseCase.setCannedMessages(destNum, messages) }
|
||||
}
|
||||
|
||||
private fun getCannedMessages(destNum: Int) {
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getCannedMessages(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeviceConnectionStatus(destNum: Int) {
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getDeviceConnectionStatus(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestShutdown(destNum: Int) {
|
||||
viewModelScope.launch {
|
||||
val packetId = adminActionsUseCase.shutdown(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestReboot(destNum: Int) {
|
||||
viewModelScope.launch {
|
||||
val packetId = adminActionsUseCase.reboot(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestFactoryReset(destNum: Int) {
|
||||
viewModelScope.launch {
|
||||
val isLocal = (destNum == myNodeNum)
|
||||
val packetId = adminActionsUseCase.factoryReset(destNum, isLocal)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestNodedbReset(destNum: Int, preserveFavorites: Boolean) {
|
||||
viewModelScope.launch {
|
||||
val isLocal = (destNum == myNodeNum)
|
||||
val packetId = adminActionsUseCase.nodedbReset(destNum, preserveFavorites, isLocal)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendAdminRequest(destNum: Int) {
|
||||
val route = radioConfigState.value.route
|
||||
_radioConfigState.update { it.copy(route = "") } // setter (response is PortNum.ROUTING_APP)
|
||||
|
|
@ -426,18 +319,35 @@ constructor(
|
|||
val preserveFavorites = radioConfigState.value.nodeDbResetPreserveFavorites
|
||||
|
||||
when (route) {
|
||||
AdminRoute.REBOOT.name -> requestReboot(destNum)
|
||||
AdminRoute.REBOOT.name ->
|
||||
viewModelScope.launch {
|
||||
val packetId = adminActionsUseCase.reboot(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
AdminRoute.SHUTDOWN.name ->
|
||||
with(radioConfigState.value) {
|
||||
if (metadata?.canShutdown != true) {
|
||||
sendError(Res.string.cant_shutdown)
|
||||
} else {
|
||||
requestShutdown(destNum)
|
||||
viewModelScope.launch {
|
||||
val packetId = adminActionsUseCase.shutdown(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AdminRoute.FACTORY_RESET.name -> requestFactoryReset(destNum)
|
||||
AdminRoute.NODEDB_RESET.name -> requestNodedbReset(destNum, preserveFavorites)
|
||||
AdminRoute.FACTORY_RESET.name ->
|
||||
viewModelScope.launch {
|
||||
val isLocal = (destNum == myNodeNum)
|
||||
val packetId = adminActionsUseCase.factoryReset(destNum, isLocal)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
AdminRoute.NODEDB_RESET.name ->
|
||||
viewModelScope.launch {
|
||||
val isLocal = (destNum == myNodeNum)
|
||||
val packetId = adminActionsUseCase.nodedbReset(destNum, preserveFavorites, isLocal)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -451,50 +361,16 @@ constructor(
|
|||
viewModelScope.launch { radioConfigUseCase.removeFixedPosition(destNum) }
|
||||
}
|
||||
|
||||
fun importProfile(uri: Uri, onResult: (DeviceProfile) -> Unit) = viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
app.contentResolver.openInputStream(uri)?.source()?.buffer()?.use { inputStream ->
|
||||
importProfileUseCase(inputStream).onSuccess(onResult).onFailure { throw it }
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Logger.e { "Import DeviceProfile error: ${ex.message}" }
|
||||
sendError(ex.customMessage)
|
||||
}
|
||||
open fun importProfile(uri: Any, onResult: (DeviceProfile) -> Unit) {
|
||||
// To be implemented in platform-specific subclass
|
||||
}
|
||||
|
||||
fun exportProfile(uri: Uri, profile: DeviceProfile) = viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream ->
|
||||
exportProfileUseCase(outputStream, profile)
|
||||
.onSuccess { setResponseStateSuccess() }
|
||||
.onFailure { throw it }
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Logger.e { "Can't write file error: ${ex.message}" }
|
||||
sendError(ex.customMessage)
|
||||
}
|
||||
}
|
||||
open fun exportProfile(uri: Any, profile: DeviceProfile) {
|
||||
// To be implemented in platform-specific subclass
|
||||
}
|
||||
|
||||
fun exportSecurityConfig(uri: Uri, securityConfig: Config.SecurityConfig) = viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
app.contentResolver.openFileDescriptor(uri, "wt")?.use { parcelFileDescriptor ->
|
||||
FileOutputStream(parcelFileDescriptor.fileDescriptor).sink().buffer().use { outputStream ->
|
||||
exportSecurityConfigUseCase(outputStream, securityConfig)
|
||||
.onSuccess { setResponseStateSuccess() }
|
||||
.onFailure { throw it }
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
val errorMessage = "Can't write security keys JSON error: ${ex.message}"
|
||||
Logger.e { errorMessage }
|
||||
sendError(ex.customMessage)
|
||||
}
|
||||
}
|
||||
open fun exportSecurityConfig(uri: Any, securityConfig: Config.SecurityConfig) {
|
||||
// To be implemented in platform-specific subclass
|
||||
}
|
||||
|
||||
fun installProfile(protobuf: DeviceProfile) {
|
||||
|
|
@ -513,38 +389,70 @@ constructor(
|
|||
_radioConfigState.update { it.copy(route = route.name, responseState = ResponseState.Loading()) }
|
||||
|
||||
when (route) {
|
||||
ConfigRoute.USER -> getOwner(destNum)
|
||||
ConfigRoute.USER ->
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getOwner(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
|
||||
ConfigRoute.CHANNELS -> {
|
||||
getChannel(destNum, 0)
|
||||
getConfig(destNum, AdminMessage.ConfigType.LORA_CONFIG.value)
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getChannel(destNum, 0)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getConfig(destNum, AdminMessage.ConfigType.LORA_CONFIG.value)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
// channel editor is synchronous, so we don't use requestIds as total
|
||||
setResponseStateTotal(maxChannels + 1)
|
||||
}
|
||||
|
||||
is AdminRoute -> {
|
||||
getConfig(destNum, AdminMessage.ConfigType.SESSIONKEY_CONFIG.value)
|
||||
viewModelScope.launch {
|
||||
val packetId =
|
||||
radioConfigUseCase.getConfig(destNum, AdminMessage.ConfigType.SESSIONKEY_CONFIG.value)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
setResponseStateTotal(2)
|
||||
}
|
||||
|
||||
is ConfigRoute -> {
|
||||
if (route == ConfigRoute.LORA) {
|
||||
getChannel(destNum, 0)
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getChannel(destNum, 0)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
if (route == ConfigRoute.NETWORK) {
|
||||
getDeviceConnectionStatus(destNum)
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getDeviceConnectionStatus(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getConfig(destNum, route.type)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
getConfig(destNum, route.type)
|
||||
}
|
||||
|
||||
is ModuleRoute -> {
|
||||
if (route == ModuleRoute.CANNED_MESSAGE) {
|
||||
getCannedMessages(destNum)
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getCannedMessages(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
if (route == ModuleRoute.EXT_NOTIFICATION) {
|
||||
getRingtone(destNum)
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getRingtone(destNum)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getModuleConfig(destNum, route.type)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
getModuleConfig(destNum, route.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -565,7 +473,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun setResponseStateSuccess() {
|
||||
protected fun setResponseStateSuccess() {
|
||||
_radioConfigState.update { state ->
|
||||
if (state.responseState is ResponseState.Loading) {
|
||||
state.copy(responseState = ResponseState.Success(true))
|
||||
|
|
@ -575,14 +483,11 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private val Exception.customMessage: String
|
||||
get() = "${javaClass.simpleName}: $message"
|
||||
protected fun sendError(error: String) = setResponseStateError(UiText.DynamicString(error))
|
||||
|
||||
private fun sendError(error: String) = setResponseStateError(UiText.DynamicString(error))
|
||||
protected fun sendError(id: StringResource) = setResponseStateError(UiText.Resource(id))
|
||||
|
||||
private fun sendError(id: StringResource) = setResponseStateError(UiText.Resource(id))
|
||||
|
||||
private fun sendError(error: UiText) = setResponseStateError(error)
|
||||
protected fun sendError(error: UiText) = setResponseStateError(error)
|
||||
|
||||
private fun setResponseStateError(error: UiText) {
|
||||
_radioConfigState.update { it.copy(responseState = ResponseState.Error(error)) }
|
||||
|
|
@ -658,7 +563,10 @@ constructor(
|
|||
val index = response.index
|
||||
if (index + 1 < maxChannels && route == ConfigRoute.CHANNELS.name) {
|
||||
// Not done yet, request next channel
|
||||
getChannel(destNum, index + 1)
|
||||
viewModelScope.launch {
|
||||
val packetId = radioConfigUseCase.getChannel(destNum, index + 1)
|
||||
registerRequestId(packetId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Received last channel, update total and start channel editor
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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/>.
|
||||
-->
|
||||
|
||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<locale android:name="en"/>
|
||||
<locale android:name="ar"/>
|
||||
<locale android:name="bg"/>
|
||||
<locale android:name="ca"/>
|
||||
<locale android:name="cs"/>
|
||||
<locale android:name="de"/>
|
||||
<locale android:name="el"/>
|
||||
<locale android:name="es"/>
|
||||
<locale android:name="et"/>
|
||||
<locale android:name="fi"/>
|
||||
<locale android:name="fr"/>
|
||||
<locale android:name="ga"/>
|
||||
<locale android:name="gl"/>
|
||||
<locale android:name="hr"/>
|
||||
<locale android:name="ht"/>
|
||||
<locale android:name="hu"/>
|
||||
<locale android:name="is"/>
|
||||
<locale android:name="it"/>
|
||||
<locale android:name="iw"/>
|
||||
<locale android:name="ja"/>
|
||||
<locale android:name="ko"/>
|
||||
<locale android:name="lt"/>
|
||||
<locale android:name="nl"/>
|
||||
<locale android:name="nb"/>
|
||||
<locale android:name="pl"/>
|
||||
<locale android:name="pt"/>
|
||||
<locale android:name="pt-BR"/>
|
||||
<locale android:name="ro"/>
|
||||
<locale android:name="ru"/>
|
||||
<locale android:name="sk"/>
|
||||
<locale android:name="sl"/>
|
||||
<locale android:name="sq"/>
|
||||
<locale android:name="sr"/>
|
||||
<locale android:name="srp"/>
|
||||
<locale android:name="sv"/>
|
||||
<locale android:name="tr"/>
|
||||
<locale android:name="uk"/>
|
||||
<locale android:name="zh-CN"/>
|
||||
<locale android:name="zh-TW"/>
|
||||
</locale-config>
|
||||
Loading…
Add table
Add a link
Reference in a new issue