From a67927818b6af66a5b3bbba7dc31ba1b98e1cee8 Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Fri, 22 May 2026 17:01:50 -0700
Subject: [PATCH] Extract node list display settings to dedicated screen
(#5580)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../org/meshtastic/core/navigation/Routes.kt | 2 +
.../feature/settings/SettingsScreen.kt | 44 ++----
.../feature/settings/NodeListScreen.kt | 90 +++++++++++
.../feature/settings/SettingsViewModel.kt | 60 +++++++
.../settings/component/NodeLayoutSettings.kt | 146 +++++++++---------
.../component/NodeLayoutSettingsPreviews.kt | 70 +++++----
.../settings/navigation/SettingsNavigation.kt | 13 +-
.../feature/settings/DesktopSettingsScreen.kt | 44 ++----
...SettingsCompactMinimal_Dark_d19fbf1f_0.png | Bin 118134 -> 123720 bytes
...ettingsCompactMinimal_Light_b29dc7a7_0.png | Bin 117662 -> 122904 bytes
...eLayoutSettingsCompact_Dark_d19fbf1f_0.png | Bin 137870 -> 142479 bytes
...LayoutSettingsCompact_Light_b29dc7a7_0.png | Bin 136087 -> 140195 bytes
12 files changed, 295 insertions(+), 174 deletions(-)
create mode 100644 feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/NodeListScreen.kt
diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt
index 9c140181a..964caec99 100644
--- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt
+++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt
@@ -166,6 +166,8 @@ sealed interface SettingsRoute : Route {
@Serializable data object FilterSettings : SettingsRoute
+ @Serializable data object NodeList : SettingsRoute
+
// endregion
// region help & documentation routes
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt
index 5ae16264c..d894e7644 100644
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.kt
@@ -49,6 +49,7 @@ import org.meshtastic.core.resources.export_configuration
import org.meshtastic.core.resources.filter_settings
import org.meshtastic.core.resources.help_and_documentation
import org.meshtastic.core.resources.import_configuration
+import org.meshtastic.core.resources.node_layout_section_title
import org.meshtastic.core.resources.preferences_language
import org.meshtastic.core.resources.remotely_administrating
import org.meshtastic.core.resources.wifi_devices
@@ -57,12 +58,12 @@ import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.MeshtasticDialog
import org.meshtastic.core.ui.icon.FilterList
import org.meshtastic.core.ui.icon.HelpOutline
+import org.meshtastic.core.ui.icon.List
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Wifi
import org.meshtastic.feature.settings.component.AppInfoSection
import org.meshtastic.feature.settings.component.AppearanceSection
import org.meshtastic.feature.settings.component.ExpressiveSection
-import org.meshtastic.feature.settings.component.NodeLayoutSettings
import org.meshtastic.feature.settings.component.PersistenceSection
import org.meshtastic.feature.settings.component.PrivacySection
import org.meshtastic.feature.settings.component.ThemePickerDialog
@@ -241,39 +242,14 @@ fun SettingsScreen(
onShowThemePicker = { showThemePickerDialog = true },
)
- val densityName by settingsViewModel.nodeListDensity.collectAsStateWithLifecycle()
- val density = org.meshtastic.core.model.NodeListDensity.fromName(densityName)
- val showPower by settingsViewModel.shouldShowPower.collectAsStateWithLifecycle()
- val showLastHeard by settingsViewModel.shouldShowLastHeard.collectAsStateWithLifecycle()
- val lastHeardRelative by settingsViewModel.lastHeardIsRelative.collectAsStateWithLifecycle()
- val showLocation by settingsViewModel.shouldShowLocation.collectAsStateWithLifecycle()
- val showHops by settingsViewModel.shouldShowHops.collectAsStateWithLifecycle()
- val showSignal by settingsViewModel.shouldShowSignal.collectAsStateWithLifecycle()
- val showChannel by settingsViewModel.shouldShowChannel.collectAsStateWithLifecycle()
- val showRole by settingsViewModel.shouldShowRole.collectAsStateWithLifecycle()
- val showTelemetry by settingsViewModel.shouldShowTelemetry.collectAsStateWithLifecycle()
- NodeLayoutSettings(
- density = density,
- onDensityChange = { settingsViewModel.setNodeListDensity(it.name) },
- showPower = showPower,
- onShowPowerChange = { settingsViewModel.setShouldShowPower(it) },
- showLastHeard = showLastHeard,
- onShowLastHeardChange = { settingsViewModel.setShouldShowLastHeard(it) },
- lastHeardIsRelative = lastHeardRelative,
- onLastHeardIsRelativeChange = { settingsViewModel.setLastHeardIsRelative(it) },
- showLocation = showLocation,
- onShowLocationChange = { settingsViewModel.setShouldShowLocation(it) },
- showHops = showHops,
- onShowHopsChange = { settingsViewModel.setShouldShowHops(it) },
- showSignal = showSignal,
- onShowSignalChange = { settingsViewModel.setShouldShowSignal(it) },
- showChannel = showChannel,
- onShowChannelChange = { settingsViewModel.setShouldShowChannel(it) },
- showRole = showRole,
- onShowRoleChange = { settingsViewModel.setShouldShowRole(it) },
- showTelemetry = showTelemetry,
- onShowTelemetryChange = { settingsViewModel.setShouldShowTelemetry(it) },
- )
+ ExpressiveSection(title = stringResource(Res.string.node_layout_section_title)) {
+ ListItem(
+ text = stringResource(Res.string.node_layout_section_title),
+ leadingIcon = MeshtasticIcons.List,
+ ) {
+ onNavigate(SettingsRoute.NodeList)
+ }
+ }
ExpressiveSection(title = stringResource(Res.string.wifi_devices)) {
ListItem(text = stringResource(Res.string.wifi_devices), leadingIcon = MeshtasticIcons.Wifi) {
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/NodeListScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/NodeListScreen.kt
new file mode 100644
index 000000000..a5a02e9a5
--- /dev/null
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/NodeListScreen.kt
@@ -0,0 +1,90 @@
+/*
+ * 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 .
+ */
+@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
+
+package org.meshtastic.feature.settings
+
+import androidx.compose.foundation.layout.Arrangement
+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.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import org.jetbrains.compose.resources.stringResource
+import org.meshtastic.core.resources.Res
+import org.meshtastic.core.resources.node_layout_section_title
+import org.meshtastic.core.ui.component.MainAppBar
+import org.meshtastic.feature.settings.component.NodeLayoutSettings
+
+/**
+ * Dedicated settings screen for node list display options (density and field visibility). Provides a focused interface
+ * for customizing how nodes are rendered in the list view.
+ *
+ * Material 3 expressive design highlights:
+ * - **Section organization**: NodeLayoutSettings component handles grouped layout with cards
+ * - **Visual hierarchy**: MainAppBar provides clear screen identity and navigation
+ * - **Spacing rhythm**: 16dp content padding with 16dp section gaps for consistent rhythm
+ * - **Scrollability**: Full vertical scroll support for all display variations
+ * - **Responsive preview**: Live preview updates as density and field options change
+ *
+ * @param settingsViewModel Provides access to node display preferences and update methods
+ * @param onNavigateUp Callback when user requests navigation back (back button in app bar)
+ */
+@Composable
+fun NodeListScreen(settingsViewModel: SettingsViewModel, onNavigateUp: () -> Unit, modifier: Modifier = Modifier) {
+ val settings by settingsViewModel.nodeListSettings.collectAsStateWithLifecycle()
+
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ MainAppBar(
+ title = stringResource(Res.string.node_layout_section_title),
+ canNavigateUp = true,
+ onNavigateUp = onNavigateUp,
+ ourNode = null,
+ showNodeChip = false,
+ actions = {},
+ onClickChip = {},
+ )
+ },
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier.verticalScroll(rememberScrollState()).padding(paddingValues).padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ NodeLayoutSettings(
+ state = settings,
+ onDensityChange = { settingsViewModel.setNodeListDensity(it.name) },
+ onShowPowerChange = { settingsViewModel.setShouldShowPower(it) },
+ onShowLastHeardChange = { settingsViewModel.setShouldShowLastHeard(it) },
+ onLastHeardIsRelativeChange = { settingsViewModel.setLastHeardIsRelative(it) },
+ onShowLocationChange = { settingsViewModel.setShouldShowLocation(it) },
+ onShowHopsChange = { settingsViewModel.setShouldShowHops(it) },
+ onShowSignalChange = { settingsViewModel.setShouldShowSignal(it) },
+ onShowChannelChange = { settingsViewModel.setShouldShowChannel(it) },
+ onShowRoleChange = { settingsViewModel.setShouldShowRole(it) },
+ onShowTelemetryChange = { settingsViewModel.setShouldShowTelemetry(it) },
+ )
+ }
+ }
+}
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt
index afd92c322..fa54c8992 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt
@@ -20,6 +20,7 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -42,6 +43,7 @@ import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.Node
+import org.meshtastic.core.model.NodeListDensity
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.FileService
import org.meshtastic.core.repository.MeshLogPrefs
@@ -223,4 +225,62 @@ class SettingsViewModel(
fun setShouldShowRole(value: Boolean) = uiPrefs.setShouldShowRole(value)
fun setShouldShowTelemetry(value: Boolean) = uiPrefs.setShouldShowTelemetry(value)
+
+ // Aggregated node list settings — nested combines because typed overloads max at 5 args
+ val nodeListSettings =
+ combine(
+ combine(
+ nodeListDensity,
+ shouldShowPower,
+ shouldShowLastHeard,
+ lastHeardIsRelative,
+ shouldShowLocation,
+ ) { density, power, lastHeard, heardRelative, location ->
+ NodeListSettingsState(
+ density = NodeListDensity.fromName(density),
+ showPower = power,
+ showLastHeard = lastHeard,
+ lastHeardIsRelative = heardRelative,
+ showLocation = location,
+ )
+ },
+ combine(shouldShowHops, shouldShowSignal, shouldShowChannel, shouldShowRole, shouldShowTelemetry) {
+ hops,
+ signal,
+ channel,
+ role,
+ telemetry,
+ ->
+ NodeListSettingsState(
+ showHops = hops,
+ showSignal = signal,
+ showChannel = channel,
+ showRole = role,
+ showTelemetry = telemetry,
+ )
+ },
+ ) { first, second ->
+ first.copy(
+ showHops = second.showHops,
+ showSignal = second.showSignal,
+ showChannel = second.showChannel,
+ showRole = second.showRole,
+ showTelemetry = second.showTelemetry,
+ )
+ }
+ .stateInWhileSubscribed(initialValue = NodeListSettingsState())
}
+
+/** Aggregated state for node list display settings to reduce recomposition overhead. */
+data class NodeListSettingsState(
+ val density: NodeListDensity = NodeListDensity.COMPLETE,
+ val showPower: Boolean = false,
+ val showLastHeard: Boolean = false,
+ val lastHeardIsRelative: Boolean = false,
+ val showLocation: Boolean = false,
+ val showHops: Boolean = false,
+ val showSignal: Boolean = false,
+ val showChannel: Boolean = false,
+ val showRole: Boolean = false,
+ val showTelemetry: Boolean = false,
+)
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettings.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettings.kt
index 624030c23..82b87859a 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettings.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettings.kt
@@ -22,6 +22,8 @@ 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.material3.Card
+import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton
@@ -57,6 +59,7 @@ import org.meshtastic.core.resources.node_layout_signal_direct_only
import org.meshtastic.core.ui.component.NodeItem
import org.meshtastic.core.ui.component.NodeItemCompact
import org.meshtastic.core.ui.component.SwitchPreference
+import org.meshtastic.feature.settings.NodeListSettingsState
import org.meshtastic.proto.Config
import org.meshtastic.proto.DeviceMetrics
import org.meshtastic.proto.EnvironmentMetrics
@@ -68,25 +71,16 @@ import org.meshtastic.proto.User
@Composable
@Suppress("LongParameterList", "LongMethod")
fun NodeLayoutSettings(
- density: NodeListDensity,
+ state: NodeListSettingsState,
onDensityChange: (NodeListDensity) -> Unit,
- showPower: Boolean,
onShowPowerChange: (Boolean) -> Unit,
- showLastHeard: Boolean,
onShowLastHeardChange: (Boolean) -> Unit,
- lastHeardIsRelative: Boolean,
onLastHeardIsRelativeChange: (Boolean) -> Unit,
- showLocation: Boolean,
onShowLocationChange: (Boolean) -> Unit,
- showHops: Boolean,
onShowHopsChange: (Boolean) -> Unit,
- showSignal: Boolean,
onShowSignalChange: (Boolean) -> Unit,
- showChannel: Boolean,
onShowChannelChange: (Boolean) -> Unit,
- showRole: Boolean,
onShowRoleChange: (Boolean) -> Unit,
- showTelemetry: Boolean,
onShowTelemetryChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -104,7 +98,7 @@ fun NodeLayoutSettings(
SegmentedButton(
shape = SegmentedButtonDefaults.itemShape(index, NodeListDensity.entries.size),
onClick = { onDensityChange(option) },
- selected = density == option,
+ selected = state.density == option,
label = { Text(label) },
)
}
@@ -122,7 +116,7 @@ fun NodeLayoutSettings(
val localNode = remember { previewLocalNode() }
Box(modifier = Modifier.animateContentSize().padding(bottom = 8.dp)) {
- when (density) {
+ when (state.density) {
NodeListDensity.COMPLETE ->
NodeItem(
thisNode = localNode,
@@ -130,7 +124,7 @@ fun NodeLayoutSettings(
distanceUnits = 0,
tempInFahrenheit = false,
connectionState = ConnectionState.Connected,
- showTelemetry = showTelemetry,
+ showTelemetry = state.showTelemetry,
)
NodeListDensity.COMPACT ->
@@ -138,15 +132,15 @@ fun NodeLayoutSettings(
thisNode = localNode,
thatNode = previewNode,
distanceUnits = 0,
- showPower = showPower,
- showLastHeard = showLastHeard,
- lastHeardIsRelative = lastHeardIsRelative,
- showLocation = showLocation,
- showHops = showHops,
- showSignal = showSignal,
- showChannel = showChannel,
- showRole = showRole,
- showTelemetry = showTelemetry,
+ showPower = state.showPower,
+ showLastHeard = state.showLastHeard,
+ lastHeardIsRelative = state.lastHeardIsRelative,
+ showLocation = state.showLocation,
+ showHops = state.showHops,
+ showSignal = state.showSignal,
+ showChannel = state.showChannel,
+ showRole = state.showRole,
+ showTelemetry = state.showTelemetry,
)
}
}
@@ -157,12 +151,12 @@ fun NodeLayoutSettings(
// Shared toggle — applies to both layouts
SwitchPreference(
title = stringResource(Res.string.node_layout_log_icons),
- checked = showTelemetry,
+ checked = state.showTelemetry,
enabled = true,
onCheckedChange = onShowTelemetryChange,
)
- if (density == NodeListDensity.COMPLETE) {
+ if (state.density == NodeListDensity.COMPLETE) {
Text(
text = stringResource(Res.string.node_layout_complete_description),
style = MaterialTheme.typography.bodyMedium,
@@ -176,56 +170,62 @@ fun NodeLayoutSettings(
text = stringResource(Res.string.node_layout_compact_fields_header),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 4.dp),
- )
- SwitchPreference(
- title = stringResource(Res.string.node_layout_power),
- checked = showPower,
- enabled = true,
- onCheckedChange = onShowPowerChange,
- )
- SwitchPreference(
- title = stringResource(Res.string.node_layout_last_heard_time),
- checked = showLastHeard,
- enabled = true,
- onCheckedChange = onShowLastHeardChange,
- )
- SwitchPreference(
- title = stringResource(Res.string.node_layout_relative_last_heard),
- checked = lastHeardIsRelative,
- enabled = showLastHeard,
- onCheckedChange = onLastHeardIsRelativeChange,
- )
- SwitchPreference(
- title = stringResource(Res.string.node_layout_distance_and_bearing),
- checked = showLocation,
- enabled = true,
- onCheckedChange = onShowLocationChange,
- )
- SwitchPreference(
- title = stringResource(Res.string.node_layout_hops_away),
- checked = showHops,
- enabled = true,
- onCheckedChange = onShowHopsChange,
- )
- SwitchPreference(
- title = stringResource(Res.string.node_layout_signal_direct_only),
- checked = showSignal,
- enabled = true,
- onCheckedChange = onShowSignalChange,
- )
- SwitchPreference(
- title = stringResource(Res.string.node_layout_channel),
- checked = showChannel,
- enabled = true,
- onCheckedChange = onShowChannelChange,
- )
- SwitchPreference(
- title = stringResource(Res.string.node_layout_device_role),
- checked = showRole,
- enabled = true,
- onCheckedChange = onShowRoleChange,
+ modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 8.dp),
)
+ Card(
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
+ ) {
+ SwitchPreference(
+ title = stringResource(Res.string.node_layout_power),
+ checked = state.showPower,
+ enabled = true,
+ onCheckedChange = onShowPowerChange,
+ )
+ SwitchPreference(
+ title = stringResource(Res.string.node_layout_last_heard_time),
+ checked = state.showLastHeard,
+ enabled = true,
+ onCheckedChange = onShowLastHeardChange,
+ )
+ SwitchPreference(
+ title = stringResource(Res.string.node_layout_relative_last_heard),
+ checked = state.lastHeardIsRelative,
+ enabled = state.showLastHeard,
+ onCheckedChange = onLastHeardIsRelativeChange,
+ )
+ SwitchPreference(
+ title = stringResource(Res.string.node_layout_distance_and_bearing),
+ checked = state.showLocation,
+ enabled = true,
+ onCheckedChange = onShowLocationChange,
+ )
+ SwitchPreference(
+ title = stringResource(Res.string.node_layout_hops_away),
+ checked = state.showHops,
+ enabled = true,
+ onCheckedChange = onShowHopsChange,
+ )
+ SwitchPreference(
+ title = stringResource(Res.string.node_layout_signal_direct_only),
+ checked = state.showSignal,
+ enabled = true,
+ onCheckedChange = onShowSignalChange,
+ )
+ SwitchPreference(
+ title = stringResource(Res.string.node_layout_channel),
+ checked = state.showChannel,
+ enabled = true,
+ onCheckedChange = onShowChannelChange,
+ )
+ SwitchPreference(
+ title = stringResource(Res.string.node_layout_device_role),
+ checked = state.showRole,
+ enabled = true,
+ onCheckedChange = onShowRoleChange,
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
}
}
}
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettingsPreviews.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettingsPreviews.kt
index 82dab3551..fe9c94b02 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettingsPreviews.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/NodeLayoutSettingsPreviews.kt
@@ -33,6 +33,7 @@ import org.meshtastic.core.model.NodeListDensity
import org.meshtastic.core.ui.component.NodeItem
import org.meshtastic.core.ui.component.NodeItemCompact
import org.meshtastic.core.ui.theme.AppTheme
+import org.meshtastic.feature.settings.NodeListSettingsState
@PreviewLightDark
@Composable
@@ -40,25 +41,28 @@ fun NodeLayoutSettingsCompactPreview() {
AppTheme {
Surface {
NodeLayoutSettings(
- density = NodeListDensity.COMPACT,
+ state =
+ NodeListSettingsState(
+ density = NodeListDensity.COMPACT,
+ showPower = true,
+ showLastHeard = true,
+ lastHeardIsRelative = true,
+ showLocation = true,
+ showHops = true,
+ showSignal = true,
+ showChannel = false,
+ showRole = true,
+ showTelemetry = true,
+ ),
onDensityChange = {},
- showPower = true,
onShowPowerChange = {},
- showLastHeard = true,
onShowLastHeardChange = {},
- lastHeardIsRelative = true,
onLastHeardIsRelativeChange = {},
- showLocation = true,
onShowLocationChange = {},
- showHops = true,
onShowHopsChange = {},
- showSignal = true,
onShowSignalChange = {},
- showChannel = false,
onShowChannelChange = {},
- showRole = true,
onShowRoleChange = {},
- showTelemetry = true,
onShowTelemetryChange = {},
)
}
@@ -71,25 +75,28 @@ fun NodeLayoutSettingsCompletePreview() {
AppTheme {
Surface {
NodeLayoutSettings(
- density = NodeListDensity.COMPLETE,
+ state =
+ NodeListSettingsState(
+ density = NodeListDensity.COMPLETE,
+ showPower = true,
+ showLastHeard = true,
+ lastHeardIsRelative = true,
+ showLocation = true,
+ showHops = true,
+ showSignal = true,
+ showChannel = true,
+ showRole = true,
+ showTelemetry = true,
+ ),
onDensityChange = {},
- showPower = true,
onShowPowerChange = {},
- showLastHeard = true,
onShowLastHeardChange = {},
- lastHeardIsRelative = true,
onLastHeardIsRelativeChange = {},
- showLocation = true,
onShowLocationChange = {},
- showHops = true,
onShowHopsChange = {},
- showSignal = true,
onShowSignalChange = {},
- showChannel = true,
onShowChannelChange = {},
- showRole = true,
onShowRoleChange = {},
- showTelemetry = true,
onShowTelemetryChange = {},
)
}
@@ -103,25 +110,28 @@ fun NodeLayoutSettingsCompactMinimalPreview() {
AppTheme {
Surface {
NodeLayoutSettings(
- density = NodeListDensity.COMPACT,
+ state =
+ NodeListSettingsState(
+ density = NodeListDensity.COMPACT,
+ showPower = false,
+ showLastHeard = true,
+ lastHeardIsRelative = true,
+ showLocation = false,
+ showHops = false,
+ showSignal = true,
+ showChannel = false,
+ showRole = false,
+ showTelemetry = false,
+ ),
onDensityChange = {},
- showPower = false,
onShowPowerChange = {},
- showLastHeard = true,
onShowLastHeardChange = {},
- lastHeardIsRelative = true,
onLastHeardIsRelativeChange = {},
- showLocation = false,
onShowLocationChange = {},
- showHops = false,
onShowHopsChange = {},
- showSignal = true,
onShowSignalChange = {},
- showChannel = false,
onShowChannelChange = {},
- showRole = false,
onShowRoleChange = {},
- showTelemetry = false,
onShowTelemetryChange = {},
)
}
diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt
index 790b13f1f..fb300eb4f 100644
--- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt
+++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt
@@ -34,6 +34,7 @@ import org.meshtastic.feature.settings.AboutScreen
import org.meshtastic.feature.settings.AdministrationScreen
import org.meshtastic.feature.settings.DeviceConfigurationScreen
import org.meshtastic.feature.settings.ModuleConfigurationScreen
+import org.meshtastic.feature.settings.NodeListScreen
import org.meshtastic.feature.settings.SettingsViewModel
import org.meshtastic.feature.settings.debugging.DebugScreen
import org.meshtastic.feature.settings.debugging.DebugViewModel
@@ -78,9 +79,7 @@ fun getRadioConfigViewModel(backStack: NavBackStack, destNumOverride: In
?: remember(backStack.toList()) {
backStack.lastOrNull { it is SettingsRoute.Settings }?.let { (it as SettingsRoute.Settings).destNum }
}
- return koinViewModel(key = destNum?.toString()) {
- parametersOf(destNum)
- }
+ return koinViewModel(key = destNum?.toString()) { parametersOf(destNum) }
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
@@ -243,6 +242,14 @@ fun EntryProviderScope.settingsGraph(backStack: NavBackStack) {
val viewModel: FilterSettingsViewModel = koinViewModel()
FilterSettingsScreen(viewModel = viewModel, onBack = dropUnlessResumed { backStack.removeLastOrNull() })
}
+
+ entry {
+ val settingsViewModel: SettingsViewModel = koinViewModel()
+ NodeListScreen(
+ settingsViewModel = settingsViewModel,
+ onNavigateUp = dropUnlessResumed { backStack.removeLastOrNull() },
+ )
+ }
}
/** Expect declaration for the platform-specific settings main screen. */
diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt
index 36cacaf5d..1036a0891 100644
--- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt
+++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/DesktopSettingsScreen.kt
@@ -52,6 +52,7 @@ import org.meshtastic.core.resources.help_and_documentation
import org.meshtastic.core.resources.info
import org.meshtastic.core.resources.modules_already_unlocked
import org.meshtastic.core.resources.modules_unlocked
+import org.meshtastic.core.resources.node_layout_section_title
import org.meshtastic.core.resources.preferences_language
import org.meshtastic.core.resources.remotely_administrating
import org.meshtastic.core.resources.theme
@@ -65,13 +66,13 @@ import org.meshtastic.core.ui.icon.FormatPaint
import org.meshtastic.core.ui.icon.HelpOutline
import org.meshtastic.core.ui.icon.Info
import org.meshtastic.core.ui.icon.Language
+import org.meshtastic.core.ui.icon.List
import org.meshtastic.core.ui.icon.Memory
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Wifi
import org.meshtastic.core.ui.util.rememberShowToastResource
import org.meshtastic.feature.settings.component.ExpressiveSection
import org.meshtastic.feature.settings.component.HomoglyphSetting
-import org.meshtastic.feature.settings.component.NodeLayoutSettings
import org.meshtastic.feature.settings.component.NotificationSection
import org.meshtastic.feature.settings.component.ThemePickerDialog
import org.meshtastic.feature.settings.navigation.ConfigRoute
@@ -203,39 +204,14 @@ fun DesktopSettingsScreen(
)
}
- val densityName by settingsViewModel.nodeListDensity.collectAsStateWithLifecycle()
- val density = org.meshtastic.core.model.NodeListDensity.fromName(densityName)
- val showPower by settingsViewModel.shouldShowPower.collectAsStateWithLifecycle()
- val showLastHeard by settingsViewModel.shouldShowLastHeard.collectAsStateWithLifecycle()
- val lastHeardRelative by settingsViewModel.lastHeardIsRelative.collectAsStateWithLifecycle()
- val showLocation by settingsViewModel.shouldShowLocation.collectAsStateWithLifecycle()
- val showHops by settingsViewModel.shouldShowHops.collectAsStateWithLifecycle()
- val showSignal by settingsViewModel.shouldShowSignal.collectAsStateWithLifecycle()
- val showChannel by settingsViewModel.shouldShowChannel.collectAsStateWithLifecycle()
- val showRole by settingsViewModel.shouldShowRole.collectAsStateWithLifecycle()
- val showTelemetry by settingsViewModel.shouldShowTelemetry.collectAsStateWithLifecycle()
- NodeLayoutSettings(
- density = density,
- onDensityChange = { settingsViewModel.setNodeListDensity(it.name) },
- showPower = showPower,
- onShowPowerChange = { settingsViewModel.setShouldShowPower(it) },
- showLastHeard = showLastHeard,
- onShowLastHeardChange = { settingsViewModel.setShouldShowLastHeard(it) },
- lastHeardIsRelative = lastHeardRelative,
- onLastHeardIsRelativeChange = { settingsViewModel.setLastHeardIsRelative(it) },
- showLocation = showLocation,
- onShowLocationChange = { settingsViewModel.setShouldShowLocation(it) },
- showHops = showHops,
- onShowHopsChange = { settingsViewModel.setShouldShowHops(it) },
- showSignal = showSignal,
- onShowSignalChange = { settingsViewModel.setShouldShowSignal(it) },
- showChannel = showChannel,
- onShowChannelChange = { settingsViewModel.setShouldShowChannel(it) },
- showRole = showRole,
- onShowRoleChange = { settingsViewModel.setShouldShowRole(it) },
- showTelemetry = showTelemetry,
- onShowTelemetryChange = { settingsViewModel.setShouldShowTelemetry(it) },
- )
+ ExpressiveSection(title = stringResource(Res.string.node_layout_section_title)) {
+ ListItem(
+ text = stringResource(Res.string.node_layout_section_title),
+ leadingIcon = MeshtasticIcons.List,
+ ) {
+ onNavigate(SettingsRoute.NodeList)
+ }
+ }
ExpressiveSection(title = stringResource(Res.string.wifi_devices)) {
ListItem(text = stringResource(Res.string.wifi_devices), leadingIcon = MeshtasticIcons.Wifi) {
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompactMinimal_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotNodeLayoutSettingsCompactMinimal_Dark_d19fbf1f_0.png
index 2bb7a1aca4b09d267ceefd64d5ea04ca3d504e9e..ab30b43ffb529920e5fa7a108923ae8cc260a2c2 100644
GIT binary patch
delta 82349
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