From 177138ac8f2de92171ec867ff34eebe072707f50 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:04:27 -0400 Subject: [PATCH] More migration to top-level Settings (#2903) --- .../java/com/geeksville/mesh/MainActivity.kt | 35 --- .../mesh/navigation/SettingsRoutes.kt | 4 +- .../main/java/com/geeksville/mesh/ui/Main.kt | 16 +- .../mesh/ui/common/components/MainAppBar.kt | 2 - .../mesh/ui/settings/SettingsScreen.kt | 217 ++++++++++++++ .../mesh/ui/settings/radio/RadioConfig.kt | 279 ++++-------------- app/src/main/res/values/strings.xml | 1 + 7 files changed, 279 insertions(+), 275 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index a6128be8b..ecb15d6fc 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -227,42 +227,7 @@ class MainActivity : createRangetestLauncher.launch(intent) } - MainMenuAction.THEME -> { - chooseThemeDialog() - } - - MainMenuAction.LANGUAGE -> { - chooseLangDialog() - } - else -> warn("Unexpected action: $action") } } - - private fun chooseThemeDialog() { - val styles = - mapOf( - getString(R.string.dynamic) to MODE_DYNAMIC, - getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO, - getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES, - getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, - ) - - val theme = uiPrefs.theme - debug("Theme from prefs: $theme") - model.showAlert( - title = getString(R.string.choose_theme), - message = "", - choices = styles.mapValues { (_, value) -> { model.setTheme(value) } }, - ) - } - - private fun chooseLangDialog() { - val languageTags = LanguageUtils.getLanguageTags(this) - val lang = LanguageUtils.getLocale() - debug("Lang from prefs: $lang") - val langMap = languageTags.mapValues { (_, value) -> { LanguageUtils.setLocale(value) } } - - model.showAlert(title = getString(R.string.preferences_language), message = "", choices = langMap) - } } diff --git a/app/src/main/java/com/geeksville/mesh/navigation/SettingsRoutes.kt b/app/src/main/java/com/geeksville/mesh/navigation/SettingsRoutes.kt index 91d61c3b4..1eb97d589 100644 --- a/app/src/main/java/com/geeksville/mesh/navigation/SettingsRoutes.kt +++ b/app/src/main/java/com/geeksville/mesh/navigation/SettingsRoutes.kt @@ -56,8 +56,8 @@ import com.geeksville.mesh.AdminProtos import com.geeksville.mesh.MeshProtos.DeviceMetadata import com.geeksville.mesh.R import com.geeksville.mesh.model.UIViewModel +import com.geeksville.mesh.ui.settings.SettingsScreen import com.geeksville.mesh.ui.settings.radio.CleanNodeDatabaseScreen -import com.geeksville.mesh.ui.settings.radio.RadioConfigScreen import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel import com.geeksville.mesh.ui.settings.radio.components.AmbientLightingConfigScreen import com.geeksville.mesh.ui.settings.radio.components.AudioConfigScreen @@ -161,7 +161,7 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController, uiViewModel: ) { backStackEntry -> val parentEntry = remember(backStackEntry) { navController.getBackStackEntry(SettingsRoutes.SettingsGraph::class) } - RadioConfigScreen(uiViewModel = uiViewModel, viewModel = hiltViewModel(parentEntry)) { + SettingsScreen(uiViewModel = uiViewModel, viewModel = hiltViewModel(parentEntry)) { navController.navigate(it) { popUpTo(SettingsRoutes.Settings()) { inclusive = false } } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index f46e00900..28e102abe 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -83,7 +83,6 @@ import com.geeksville.mesh.model.BluetoothViewModel import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.Node import com.geeksville.mesh.model.UIViewModel -import com.geeksville.mesh.navigation.ChannelsRoutes import com.geeksville.mesh.navigation.ConnectionsRoutes import com.geeksville.mesh.navigation.ContactsRoutes import com.geeksville.mesh.navigation.MapRoutes @@ -115,6 +114,7 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlin.reflect.KClass enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, val route: Route) { Conversations(R.string.conversations, MeshtasticIcons.Conversations, ContactsRoutes.ContactsGraph), @@ -125,14 +125,14 @@ enum class TopLevelDestination(@StringRes val label: Int, val icon: ImageVector, ; companion object { - fun NavDestination.isTopLevel(): Boolean = listOf( - ContactsRoutes.Contacts, - NodesRoutes.Nodes, - MapRoutes.Map, - ChannelsRoutes.Channels, - ConnectionsRoutes.Connections, + fun NavDestination.isTopLevel(): Boolean = listOf>( + ContactsRoutes.Contacts::class, + NodesRoutes.Nodes::class, + MapRoutes.Map::class, + ConnectionsRoutes.Connections::class, + SettingsRoutes.Settings::class, ) - .any { this.hasRoute(it::class) } + .any { this.hasRoute(it) } fun fromNavDestination(destination: NavDestination?): TopLevelDestination? = entries.find { dest -> destination?.hierarchy?.any { it.hasRoute(dest.route::class) } == true } diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt index e41879e9a..e1fae2155 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/MainAppBar.kt @@ -208,8 +208,6 @@ private fun TopBarActions( enum class MainMenuAction(@StringRes val stringRes: Int) { DEBUG(R.string.debug_panel), EXPORT_RANGETEST(R.string.save_rangetest), - THEME(R.string.theme), - LANGUAGE(R.string.preferences_language), SHOW_INTRO(R.string.intro_show), QUICK_CHAT(R.string.quick_chat), } diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt new file mode 100644 index 000000000..a4c3ba2b3 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsScreen.kt @@ -0,0 +1,217 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.ui.settings + +import android.app.Activity +import android.content.Intent +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.rounded.FormatPaint +import androidx.compose.material.icons.rounded.Language +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile +import com.geeksville.mesh.DeviceUIProtos.Language +import com.geeksville.mesh.R +import com.geeksville.mesh.android.BuildUtils.debug +import com.geeksville.mesh.model.UIViewModel +import com.geeksville.mesh.navigation.Route +import com.geeksville.mesh.navigation.getNavRouteFrom +import com.geeksville.mesh.ui.common.components.TitledCard +import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC +import com.geeksville.mesh.ui.settings.components.SettingsItem +import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch +import com.geeksville.mesh.ui.settings.radio.RadioConfigItemList +import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel +import com.geeksville.mesh.ui.settings.radio.components.EditDeviceProfileDialog +import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog +import com.geeksville.mesh.util.LanguageUtils + +@Suppress("LongMethod", "CyclomaticComplexMethod") +@Composable +fun SettingsScreen( + viewModel: RadioConfigViewModel = hiltViewModel(), + uiViewModel: UIViewModel = hiltViewModel(), + onNavigate: (Route) -> Unit = {}, +) { + uiViewModel.setTitle(stringResource(R.string.bottom_nav_settings)) + + val excludedModulesUnlocked by uiViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle() + val localConfig by uiViewModel.localConfig.collectAsStateWithLifecycle() + + val state by viewModel.radioConfigState.collectAsStateWithLifecycle() + var isWaiting by remember { mutableStateOf(false) } + if (isWaiting) { + PacketResponseStateDialog( + state = state.responseState, + onDismiss = { + isWaiting = false + viewModel.clearPacketResponse() + }, + onComplete = { + getNavRouteFrom(state.route)?.let { route -> + isWaiting = false + viewModel.clearPacketResponse() + onNavigate(route) + } + }, + ) + } + + var deviceProfile by remember { mutableStateOf(null) } + var showEditDeviceProfileDialog by remember { mutableStateOf(false) } + + val importConfigLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + showEditDeviceProfileDialog = true + it.data?.data?.let { uri -> viewModel.importProfile(uri) { profile -> deviceProfile = profile } } + } + } + + val exportConfigLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + it.data?.data?.let { uri -> viewModel.exportProfile(uri, deviceProfile!!) } + } + } + + if (showEditDeviceProfileDialog) { + EditDeviceProfileDialog( + title = + if (deviceProfile != null) { + stringResource(R.string.import_configuration) + } else { + stringResource(R.string.export_configuration) + }, + deviceProfile = deviceProfile ?: viewModel.currentDeviceProfile, + onConfirm = { + showEditDeviceProfileDialog = false + if (deviceProfile != null) { + viewModel.installProfile(it) + } else { + deviceProfile = it + val intent = + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/*" + putExtra(Intent.EXTRA_TITLE, "device_profile.cfg") + } + exportConfigLauncher.launch(intent) + } + }, + onDismiss = { + showEditDeviceProfileDialog = false + deviceProfile = null + }, + ) + } + + Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(16.dp)) { + RadioConfigItemList( + state = state, + isManaged = localConfig.security.isManaged, + excludedModulesUnlocked = excludedModulesUnlocked, + onRouteClick = { route -> + isWaiting = true + viewModel.setResponseStateLoading(route) + }, + onImport = { + viewModel.clearPacketResponse() + deviceProfile = null + val intent = + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/*" + } + importConfigLauncher.launch(intent) + }, + onExport = { + viewModel.clearPacketResponse() + deviceProfile = null + showEditDeviceProfileDialog = true + }, + onNavigate = onNavigate, + ) + + TitledCard(title = stringResource(R.string.phone_settings), modifier = Modifier.padding(top = 16.dp)) { + if (state.analyticsAvailable) { + SettingsItemSwitch( + text = stringResource(R.string.analytics_okay), + checked = state.analyticsEnabled, + leadingIcon = Icons.Default.BugReport, + onClick = { viewModel.toggleAnalytics() }, + ) + } + + val context = LocalContext.current + val languageTags = remember { LanguageUtils.getLanguageTags(context) } + SettingsItem( + text = stringResource(R.string.preferences_language), + leadingIcon = Icons.Rounded.Language, + trailingIcon = null, + ) { + val lang = LanguageUtils.getLocale() + debug("Lang from prefs: $lang") + val langMap = languageTags.mapValues { (_, value) -> { LanguageUtils.setLocale(value) } } + + uiViewModel.showAlert( + title = context.getString(R.string.preferences_language), + message = "", + choices = langMap, + ) + } + + val themeMap = remember { + mapOf( + context.getString(R.string.dynamic) to MODE_DYNAMIC, + context.getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO, + context.getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES, + context.getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, + ) + } + SettingsItem( + text = stringResource(R.string.theme), + leadingIcon = Icons.Rounded.FormatPaint, + trailingIcon = null, + ) { + uiViewModel.showAlert( + title = context.getString(R.string.choose_theme), + message = "", + choices = themeMap.mapValues { (_, value) -> { uiViewModel.setTheme(value) } }, + ) + } + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfig.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfig.kt index d7ef8119a..cd6f7cd78 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfig.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfig.kt @@ -17,35 +17,22 @@ package com.geeksville.mesh.ui.settings.radio -import android.app.Activity -import android.content.Intent import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.twotone.KeyboardArrowRight -import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Upload import androidx.compose.material.icons.twotone.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button -import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -60,14 +47,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile import com.geeksville.mesh.R import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.navigation.AdminRoute @@ -75,156 +59,13 @@ import com.geeksville.mesh.navigation.ConfigRoute import com.geeksville.mesh.navigation.ModuleRoute import com.geeksville.mesh.navigation.Route import com.geeksville.mesh.navigation.SettingsRoutes -import com.geeksville.mesh.navigation.getNavRouteFrom import com.geeksville.mesh.ui.common.components.TitledCard import com.geeksville.mesh.ui.common.theme.AppTheme import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed import com.geeksville.mesh.ui.settings.components.SettingsItem -import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch -import com.geeksville.mesh.ui.settings.radio.components.EditDeviceProfileDialog -import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.seconds -@Suppress("LongMethod", "CyclomaticComplexMethod") -@Composable -fun RadioConfigScreen( - modifier: Modifier = Modifier, - viewModel: RadioConfigViewModel = hiltViewModel(), - uiViewModel: UIViewModel = hiltViewModel(), - onNavigate: (Route) -> Unit = {}, -) { - val node by viewModel.destNode.collectAsStateWithLifecycle() - val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle() - val isLocal = node?.num == ourNode?.num - val nodeName: String? = - node?.user?.longName?.let { - if (!isLocal) { - "$it (" + stringResource(R.string.remote) + ")" - } else { - it - } - } - - nodeName?.let { uiViewModel.setTitle(it) } - - val excludedModulesUnlocked by uiViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle() - val localConfig by uiViewModel.localConfig.collectAsStateWithLifecycle() - - val state by viewModel.radioConfigState.collectAsStateWithLifecycle() - var isWaiting by remember { mutableStateOf(false) } - if (isWaiting) { - PacketResponseStateDialog( - state = state.responseState, - onDismiss = { - isWaiting = false - viewModel.clearPacketResponse() - }, - onComplete = { - getNavRouteFrom(state.route)?.let { route -> - isWaiting = false - viewModel.clearPacketResponse() - onNavigate(route) - } - }, - ) - } - - var deviceProfile by remember { mutableStateOf(null) } - var showEditDeviceProfileDialog by remember { mutableStateOf(false) } - - val importConfigLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == Activity.RESULT_OK) { - showEditDeviceProfileDialog = true - it.data?.data?.let { uri -> viewModel.importProfile(uri) { profile -> deviceProfile = profile } } - } - } - - val exportConfigLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { - if (it.resultCode == Activity.RESULT_OK) { - it.data?.data?.let { uri -> viewModel.exportProfile(uri, deviceProfile!!) } - } - } - - if (showEditDeviceProfileDialog) { - EditDeviceProfileDialog( - title = - if (deviceProfile != null) { - stringResource(R.string.import_configuration) - } else { - stringResource(R.string.export_configuration) - }, - deviceProfile = deviceProfile ?: viewModel.currentDeviceProfile, - onConfirm = { - showEditDeviceProfileDialog = false - if (deviceProfile != null) { - viewModel.installProfile(it) - } else { - deviceProfile = it - val intent = - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/*" - putExtra(Intent.EXTRA_TITLE, "device_profile.cfg") - } - exportConfigLauncher.launch(intent) - } - }, - onDismiss = { - showEditDeviceProfileDialog = false - deviceProfile = null - }, - ) - } - - RadioConfigItemList( - modifier = modifier, - state = state, - isManaged = localConfig.security.isManaged, - excludedModulesUnlocked = excludedModulesUnlocked, - onRouteClick = { route -> - isWaiting = true - viewModel.setResponseStateLoading(route) - }, - onImport = { - viewModel.clearPacketResponse() - deviceProfile = null - val intent = - Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/*" - } - importConfigLauncher.launch(intent) - }, - onExport = { - viewModel.clearPacketResponse() - deviceProfile = null - showEditDeviceProfileDialog = true - }, - onToggleAnalytics = { viewModel.toggleAnalytics() }, - onNavigate = onNavigate, - ) -} - -@Composable -fun NavCard(title: String, enabled: Boolean, icon: ImageVector? = null, onClick: () -> Unit) { - Card(onClick = onClick, enabled = enabled, modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp), - ) { - if (icon != null) { - Icon(imageVector = icon, contentDescription = title, modifier = Modifier.size(24.dp)) - Spacer(modifier = Modifier.width(8.dp)) - } - Text(text = title, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) - Icon(Icons.AutoMirrored.TwoTone.KeyboardArrowRight, "trailingIcon", modifier = Modifier.wrapContentSize()) - } - } -} - @Suppress("LongMethod") @Composable private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Unit) { @@ -281,19 +122,18 @@ private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Un @Suppress("LongMethod") @Composable -private fun RadioConfigItemList( +fun RadioConfigItemList( state: RadioConfigState, isManaged: Boolean, excludedModulesUnlocked: Boolean = false, - modifier: Modifier = Modifier, onRouteClick: (Enum<*>) -> Unit = {}, onImport: () -> Unit = {}, onExport: () -> Unit = {}, - onToggleAnalytics: () -> Unit = {}, onNavigate: (Route) -> Unit, ) { val enabled = state.connected && !state.responseState.isWaiting() && !isManaged var modules by remember { mutableStateOf(ModuleRoute.filterExcludedFrom(state.metadata)) } + LaunchedEffect(excludedModulesUnlocked) { if (excludedModulesUnlocked) { modules = ModuleRoute.entries @@ -301,85 +141,68 @@ private fun RadioConfigItemList( modules = ModuleRoute.filterExcludedFrom(state.metadata) } } - LazyColumn(modifier = modifier, contentPadding = PaddingValues(16.dp)) { - item { - TitledCard(title = stringResource(R.string.radio_configuration)) { - if (isManaged) { - ManagedMessage() - } - ConfigRoute.filterExcludedFrom(state.metadata).forEach { - SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { - onRouteClick(it) - } + Column { + TitledCard(title = stringResource(R.string.radio_configuration)) { + if (isManaged) { + ManagedMessage() + } + + ConfigRoute.filterExcludedFrom(state.metadata).forEach { + SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { + onRouteClick(it) } } } - item { - TitledCard(title = stringResource(R.string.module_settings), modifier = Modifier.padding(top = 16.dp)) { - if (isManaged) { - ManagedMessage() - } + TitledCard(title = stringResource(R.string.module_settings), modifier = Modifier.padding(top = 16.dp)) { + if (isManaged) { + ManagedMessage() + } - modules.forEach { - SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { - onRouteClick(it) - } + modules.forEach { + SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { + onRouteClick(it) } } } + } - if (state.isLocal) { - item { - TitledCard(title = stringResource(R.string.backup_restore), modifier = Modifier.padding(top = 16.dp)) { - if (isManaged) { - ManagedMessage() - } - - SettingsItem( - text = stringResource(R.string.import_configuration), - leadingIcon = Icons.Default.Download, - enabled = enabled, - onClick = onImport, - ) - SettingsItem( - text = stringResource(R.string.export_configuration), - leadingIcon = Icons.Default.Upload, - enabled = enabled, - onClick = onExport, - ) - } + if (state.isLocal) { + TitledCard(title = stringResource(R.string.backup_restore), modifier = Modifier.padding(top = 16.dp)) { + if (isManaged) { + ManagedMessage() } + + SettingsItem( + text = stringResource(R.string.import_configuration), + leadingIcon = Icons.Default.Download, + enabled = enabled, + onClick = onImport, + ) + SettingsItem( + text = stringResource(R.string.export_configuration), + leadingIcon = Icons.Default.Upload, + enabled = enabled, + onClick = onExport, + ) + } + } + + Column(modifier = Modifier.padding(top = 16.dp)) { + AdminRoute.entries.forEach { NavButton(it.title, enabled) { onRouteClick(it) } } + } + + TitledCard(title = stringResource(R.string.advanced_title), modifier = Modifier.padding(top = 16.dp)) { + if (isManaged) { + ManagedMessage() } - items(AdminRoute.entries) { NavButton(it.title, enabled) { onRouteClick(it) } } - - item { - TitledCard(title = "Advanced", modifier = Modifier.padding(top = 16.dp)) { - if (isManaged) { - ManagedMessage() - } - - SettingsItem( - text = stringResource(R.string.clean_node_database_title), - enabled = enabled, - onClick = { onNavigate(SettingsRoutes.CleanNodeDb) }, - ) - } - } - item { - if (state.analyticsAvailable) { - TitledCard(title = stringResource(R.string.phone_settings), modifier = Modifier.padding(top = 16.dp)) { - SettingsItemSwitch( - text = stringResource(R.string.analytics_okay), - checked = state.analyticsEnabled, - leadingIcon = Icons.Default.BugReport, - onClick = onToggleAnalytics, - ) - } - } - } + SettingsItem( + text = stringResource(R.string.clean_node_database_title), + enabled = enabled, + onClick = { onNavigate(SettingsRoutes.CleanNodeDb) }, + ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6939fe358..c830f5d33 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -673,6 +673,7 @@ Unknown This radio is managed and can only be changed by a remote admin. + Advanced Clean Node Database Clean up nodes last seen older than %1$d days Clean up only unknown nodes