Clean up list item component API (#3465)

This commit is contained in:
Phil Oliver 2025-10-14 14:37:59 -04:00 committed by GitHub
parent 1b9f0f9736
commit 51ccc59b24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 407 additions and 476 deletions

View file

@ -65,8 +65,8 @@ import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
@ -197,7 +197,7 @@ fun ConnectionsScreen(
if (regionUnset && selectedDevice != "m") {
TitledCard(title = null) {
SettingsItem(
ListItem(
leadingIcon = Icons.Rounded.Language,
text = stringResource(id = R.string.set_your_region),
) {

View file

@ -120,6 +120,7 @@ fun NodeDetailList(
}
NodeDetailsSection(node)
NotesSection(node = node, onSaveNotes = onSaveNotes)
DeviceActions(

View file

@ -35,9 +35,6 @@
<ID>ModifierMissing:LoraSignalIndicator.kt$SnrAndRssi</ID>
<ID>ModifierMissing:PreferenceDivider.kt$PreferenceDivider</ID>
<ID>ModifierMissing:SecurityIcon.kt$SecurityIcon</ID>
<ID>ModifierMissing:SettingsItem.kt$SettingsItem</ID>
<ID>ModifierMissing:SettingsItem.kt$SettingsItemDetail</ID>
<ID>ModifierMissing:SettingsItem.kt$SettingsItemSwitch</ID>
<ID>ModifierMissing:SharedContactDialog.kt$SharedContactDialog</ID>
<ID>ModifierMissing:SimpleAlertDialog.kt$SimpleAlertDialog</ID>
<ID>ModifierMissing:SlidingSelector.kt$OptionLabel</ID>

View file

@ -0,0 +1,181 @@
/*
* 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/>.
*/
package org.meshtastic.core.ui.component
import android.content.ClipData
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.Android
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.meshtastic.core.ui.theme.AppTheme
/**
* A list item with an optional [leadingIcon], headline [text], optional [supportingText], and optional [trailingIcon].
*/
@Composable
fun ListItem(
text: String,
modifier: Modifier = Modifier,
supportingText: String? = null,
textColor: Color = LocalContentColor.current,
supportingTextColor: Color = LocalContentColor.current,
copyable: Boolean = false,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
leadingIconTint: Color = LocalContentColor.current,
trailingIcon: ImageVector? = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
trailingIconTint: Color = LocalContentColor.current,
onClick: (() -> Unit)? = null,
) {
val clipboard: Clipboard = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
BasicListItem(
text = text,
modifier = modifier,
textColor = textColor,
supportingText = supportingText,
supportingTextColor = supportingTextColor,
enabled = enabled,
leadingIcon = leadingIcon,
leadingIconTint = leadingIconTint,
trailingContent = trailingIcon.icon(trailingIconTint),
onClick = onClick,
onLongClick =
if (!supportingText.isNullOrBlank() && copyable) {
{
coroutineScope.launch {
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", supportingText)))
}
}
} else {
null
},
)
}
/** A toggleable switch list item. */
@Composable
fun SwitchListItem(
checked: Boolean,
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
textColor: Color = LocalContentColor.current,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
leadingIconTint: Color = LocalContentColor.current,
) {
BasicListItem(
text = text,
modifier = modifier,
textColor = textColor,
enabled = enabled,
leadingIcon = leadingIcon,
leadingIconTint = leadingIconTint,
trailingContent = { Switch(checked = checked, enabled = enabled, onCheckedChange = null) },
onClick = onClick,
)
}
/**
* The foundational list item. It supports a [leadingIcon] (optional), headline [text] and [supportingText] (optional),
* and a [trailingContent] composable (optional).
*
* This is a core component that should facilitate most list item use cases. Please carefully consider if modifying this
* is really necessary before doing so.
*
* @see [LinkedCoordinatesItem] for example usage
*/
@Suppress("UnusedParameter")
@Composable
fun BasicListItem(
text: String,
modifier: Modifier = Modifier,
textColor: Color = LocalContentColor.current,
supportingText: String? = null,
supportingTextColor: Color = LocalContentColor.current,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
leadingIconTint: Color = LocalContentColor.current,
trailingContent: @Composable (() -> Unit)? = null,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
) {
ListItem(
modifier =
if (onLongClick != null || onClick != null) {
modifier.combinedClickable(onLongClick = onLongClick, onClick = onClick ?: {})
} else {
modifier
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
headlineContent = { Text(text = text, color = textColor) },
supportingContent = supportingText?.let { { Text(text = it, color = supportingTextColor) } },
leadingContent = leadingIcon.icon(leadingIconTint),
trailingContent = trailingContent,
)
}
@Composable
fun ImageVector?.icon(tint: Color = LocalContentColor.current): @Composable (() -> Unit)? =
this?.let { { Icon(imageVector = it, contentDescription = null, modifier = Modifier.size(24.dp), tint = tint) } }
@Preview(showBackground = true)
@Composable
private fun ListItemPreview() {
AppTheme { ListItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = true) {} }
}
@Preview(showBackground = true)
@Composable
private fun ListItemDisabledPreview() {
AppTheme { ListItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = false) {} }
}
@Preview(showBackground = true)
@Composable
private fun SwitchListItemPreview() {
AppTheme { SwitchListItem(text = "Text", leadingIcon = Icons.Rounded.Android, checked = true, onClick = {}) }
}
@Preview(showBackground = true)
@Composable
private fun ListItemPreviewSupportingText() {
AppTheme {
ListItem(text = "Text 1", leadingIcon = Icons.Rounded.Android, supportingText = "Text2", trailingIcon = null)
}
}

View file

@ -1,211 +0,0 @@
/*
* 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/>.
*/
package org.meshtastic.core.ui.component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.Android
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.ui.theme.AppTheme
/**
* A clickable settings item with optional supporting text and trailing content. Defaults to a trailing arrow icon if no
* custom trailing content is provided.
*/
@Composable
fun SettingsItem(
text: String,
supportingText: String? = null,
textColor: Color = LocalContentColor.current,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
leadingIconTint: Color = LocalContentColor.current,
trailingContent: @Composable (() -> Unit)? = null,
onClick: () -> Unit,
) {
val finalTrailingContent: @Composable (() -> Unit) =
trailingContent ?: { Icons.AutoMirrored.Rounded.KeyboardArrowRight.Icon(LocalContentColor.current) }
SettingsListItem(
text = text,
textColor = textColor,
enabled = enabled,
onClick = onClick,
leadingContent = { leadingIcon.Icon(leadingIconTint) },
supportingContent = { supportingText?.let { Text(text = it, style = MaterialTheme.typography.titleMedium) } },
trailingContent = finalTrailingContent,
)
}
/** A toggleable settings switch item. */
@Composable
fun SettingsItemSwitch(
checked: Boolean,
text: String,
textColor: Color = LocalContentColor.current,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
leadingIconTint: Color = LocalContentColor.current,
onClick: () -> Unit,
) {
SettingsListItem(
text = text,
textColor = textColor,
enabled = enabled,
onClick = onClick,
leadingContent = { leadingIcon.Icon(leadingIconTint) },
trailingContent = { Switch(checked = checked, enabled = enabled, onCheckedChange = null) },
)
}
/** A settings detail item. */
@Composable
fun SettingsItemDetail(
text: String,
supportingText: String?,
textColor: Color = LocalContentColor.current,
icon: ImageVector? = null,
iconTint: Color = LocalContentColor.current,
enabled: Boolean = true,
onClick: (() -> Unit)? = null,
) {
SettingsListItem(
text = text,
textColor = textColor,
enabled = enabled,
onClick = onClick,
leadingContent = { icon.Icon(iconTint) },
supportingContent = {
supportingText?.let {
Text(
text = it,
style = MaterialTheme.typography.titleLarge,
color = textColor, // Detail style explicitly sets color
)
}
},
trailingContent = {},
)
}
/** A settings detail item with composable content for the detail. */
@Composable
fun SettingsItemDetail(
text: String,
textColor: Color = LocalContentColor.current,
icon: ImageVector? = null,
iconTint: Color = LocalContentColor.current,
enabled: Boolean = true,
onClick: (() -> Unit)? = null,
supportingContent: @Composable () -> Unit,
) {
SettingsListItem(
text = text,
textColor = textColor,
enabled = enabled,
onClick = onClick,
leadingContent = { icon.Icon(iconTint) },
supportingContent = supportingContent,
trailingContent = {},
)
}
/**
* Base composable for all settings screen list items. It handles the Material3 [ListItem] structure and the conditional
* click wrapper.
*/
@Composable
private fun SettingsListItem(
text: String,
textColor: Color,
enabled: Boolean,
onClick: (() -> Unit)?,
leadingContent: @Composable (() -> Unit)? = null,
supportingContent: @Composable (() -> Unit)? = null,
trailingContent: @Composable (() -> Unit)? = null,
) {
val listItemContent: @Composable ColumnScope.() -> Unit = {
ListItem(
modifier = Modifier.padding(horizontal = 8.dp),
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
headlineContent = { Text(text = text, color = textColor) },
supportingContent = { SelectionContainer { supportingContent?.invoke() } },
leadingContent = leadingContent,
trailingContent = trailingContent,
)
}
if (onClick != null) {
Card(
onClick = onClick,
enabled = enabled,
colors =
CardDefaults.cardColors(containerColor = Color.Transparent, disabledContainerColor = Color.Transparent),
content = listItemContent,
)
} else {
Column(content = listItemContent)
}
}
@Composable
private fun ImageVector?.Icon(tint: Color = LocalContentColor.current) =
this?.let { Icon(imageVector = it, contentDescription = null, modifier = Modifier.size(24.dp), tint = tint) }
@Preview(showBackground = true)
@Composable
private fun SettingsItemPreview() {
AppTheme { SettingsItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = true) {} }
}
@Preview(showBackground = true)
@Composable
private fun SettingsItemDisabledPreview() {
AppTheme { SettingsItem(text = "Text", leadingIcon = Icons.Rounded.Android, enabled = false) {} }
}
@Preview(showBackground = true)
@Composable
private fun SettingsItemSwitchPreview() {
AppTheme { SettingsItemSwitch(text = "Text", leadingIcon = Icons.Rounded.Android, checked = true) {} }
}
@Preview(showBackground = true)
@Composable
private fun SettingsItemDetailPreview() {
AppTheme { SettingsItemDetail(text = "Text 1", icon = Icons.Rounded.Android, supportingText = "Text2") }
}

View file

@ -4,12 +4,13 @@
<CurrentIssues>
<ID>ComposableParamOrder:ElevationInfo.kt$ElevationInfo</ID>
<ID>ComposableParamOrder:LastHeardInfo.kt$LastHeardInfo</ID>
<ID>ComposableParamOrder:LinkedCoordinates.kt$LinkedCoordinates</ID>
<ID>ComposableParamOrder:NodeFilterTextField.kt$NodeFilterTextField</ID>
<ID>ComposableParamOrder:NodeItem.kt$NodeItem</ID>
<ID>ComposableParamOrder:SatelliteCountInfo.kt$SatelliteCountInfo</ID>
<ID>ComposableParamOrder:TracerouteButton.kt$TracerouteButton</ID>
<ID>ModifierMissing:NodeStatusIcons.kt$NodeStatusIcons</ID>
<ID>MultipleEmitters:NodeDetailsSection.kt$MainNodeDetails</ID>
<ID>MultipleEmitters:RemoteDeviceActions.kt$RemoteDeviceActions</ID>
<ID>ParameterNaming:NodeFilterTextField.kt$onToggleShowIgnored</ID>
<ID>PreviewPublic:NodeItem.kt$NodeInfoPreview</ID>
<ID>PreviewPublic:NodeItem.kt$NodeInfoSimplePreview</ID>

View file

@ -35,8 +35,7 @@ import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.SettingsItemDetail
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
@ -56,13 +55,13 @@ fun AdministrationSection(
modifier: Modifier = Modifier,
) {
TitledCard(stringResource(id = R.string.administration), modifier = modifier) {
SettingsItem(
ListItem(
text = stringResource(id = R.string.request_metadata),
leadingIcon = Icons.Default.Memory,
trailingContent = {},
trailingIcon = null,
onClick = { onAction(NodeDetailAction.TriggerServiceAction(ServiceAction.GetDeviceMetadata(node.num))) },
)
SettingsItem(
ListItem(
text = stringResource(id = R.string.remote_admin),
leadingIcon = Icons.Default.Settings,
enabled = metricsState.isLocal || node.metadata != null,
@ -81,10 +80,12 @@ fun AdministrationSection(
else -> Icons.Default.ForkLeft
}
SettingsItemDetail(
ListItem(
text = stringResource(R.string.firmware_edition),
icon = icon,
leadingIcon = icon,
supportingText = it.name,
copyable = true,
trailingIcon = null,
)
}
firmwareVersion?.let { firmwareVersion ->
@ -94,25 +95,31 @@ fun AdministrationSection(
val deviceVersion = DeviceVersion(firmwareVersion.substringBeforeLast("."))
val statusColor = deviceVersion.determineFirmwareStatusColor(latestStable, latestAlpha)
SettingsItemDetail(
ListItem(
text = stringResource(R.string.installed_firmware_version),
icon = Icons.Default.Memory,
leadingIcon = Icons.Default.Memory,
supportingText = firmwareVersion.substringBeforeLast("."),
iconTint = statusColor,
copyable = true,
leadingIconTint = statusColor,
trailingIcon = null,
)
HorizontalDivider()
SettingsItemDetail(
ListItem(
text = stringResource(R.string.latest_stable_firmware),
icon = Icons.Default.Memory,
leadingIcon = Icons.Default.Memory,
supportingText = latestStable.id.substringBeforeLast(".").replace("v", ""),
iconTint = MaterialTheme.colorScheme.StatusGreen,
copyable = true,
leadingIconTint = MaterialTheme.colorScheme.StatusGreen,
trailingIcon = null,
onClick = { onFirmwareSelect(latestStable) },
)
SettingsItemDetail(
ListItem(
text = stringResource(R.string.latest_alpha_firmware),
icon = Icons.Default.Memory,
leadingIcon = Icons.Default.Memory,
supportingText = latestAlpha.id.substringBeforeLast(".").replace("v", ""),
iconTint = MaterialTheme.colorScheme.StatusYellow,
copyable = true,
leadingIconTint = MaterialTheme.colorScheme.StatusYellow,
trailingIcon = null,
onClick = { onFirmwareSelect(latestAlpha) },
)
}

View file

@ -35,8 +35,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.SettingsItemSwitch
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.SwitchListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.node.model.NodeDetailAction
@ -67,33 +67,33 @@ fun DeviceActions(
onConfirmRemove = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Remove(it))) },
)
TitledCard(title = stringResource(R.string.actions), modifier = modifier) {
SettingsItem(
ListItem(
text = stringResource(id = R.string.share_contact),
leadingIcon = Icons.Rounded.QrCode2,
trailingContent = {},
trailingIcon = null,
onClick = { onAction(NodeDetailAction.ShareContact) },
)
if (!isLocal) {
RemoteDeviceActions(node = node, lastTracerouteTime = lastTracerouteTime, onAction = onAction)
}
SettingsItemSwitch(
SwitchListItem(
text = stringResource(R.string.favorite),
leadingIcon = if (node.isFavorite) Icons.Default.Star else Icons.Default.StarBorder,
leadingIconTint = if (node.isFavorite) Color.Yellow else LocalContentColor.current,
checked = node.isFavorite,
onClick = { displayFavoriteDialog = true },
)
SettingsItemSwitch(
SwitchListItem(
text = stringResource(R.string.ignore),
leadingIcon =
if (node.isIgnored) Icons.AutoMirrored.Outlined.VolumeMute else Icons.AutoMirrored.Default.VolumeUp,
checked = node.isIgnored,
onClick = { displayIgnoreDialog = true },
)
SettingsItem(
ListItem(
text = stringResource(id = R.string.remove),
leadingIcon = Icons.Rounded.Delete,
trailingContent = {},
trailingIcon = null,
onClick = { displayRemoveDialog = true },
)
}

View file

@ -45,7 +45,7 @@ import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SettingsItemDetail
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@ -71,26 +71,28 @@ fun DeviceDetailsSection(state: MetricsState, modifier: Modifier = Modifier) {
Spacer(modifier = Modifier.height(16.dp))
SettingsItemDetail(
ListItem(
text = stringResource(R.string.hardware),
icon = Icons.Default.Router,
leadingIcon = Icons.Default.Router,
supportingText = hwModelName,
copyable = true,
trailingIcon = null,
)
SettingsItemDetail(
ListItem(
text =
if (isSupported) {
stringResource(R.string.supported)
} else {
stringResource(R.string.supported_by_community)
},
icon =
leadingIcon =
if (isSupported) {
Icons.TwoTone.Verified
} else {
ImageVector.vectorResource(org.meshtastic.feature.node.R.drawable.unverified)
},
supportingText = null,
iconTint = if (isSupported) colorScheme.StatusGreen else colorScheme.StatusRed,
leadingIconTint = if (isSupported) colorScheme.StatusGreen else colorScheme.StatusRed,
trailingIcon = null,
)
}
}

View file

@ -1,119 +0,0 @@
/*
* 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/>.
*/
package org.meshtastic.feature.node.component
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.core.net.toUri
import kotlinx.coroutines.launch
import org.meshtastic.core.model.util.GPSFormat
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.HyperlinkBlue
import timber.log.Timber
import java.net.URLEncoder
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LinkedCoordinates(modifier: Modifier = Modifier, latitude: Double, longitude: Double, nodeName: String) {
val context = LocalContext.current
val clipboard: Clipboard = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
val style =
SpanStyle(
color = HyperlinkBlue,
fontStyle = MaterialTheme.typography.titleLarge.fontStyle,
textDecoration = TextDecoration.Underline,
)
val annotatedString = rememberAnnotatedString(latitude, longitude, nodeName, style)
Text(
modifier =
modifier.combinedClickable(
onClick = { handleClick(context, annotatedString) },
onLongClick = {
coroutineScope.launch {
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", annotatedString)))
Timber.d("Copied to clipboard")
}
},
),
text = annotatedString,
style = MaterialTheme.typography.titleLarge,
)
}
@Composable
private fun rememberAnnotatedString(latitude: Double, longitude: Double, nodeName: String, style: SpanStyle) =
buildAnnotatedString {
pushStringAnnotation(
tag = "gps",
annotation =
"geo:0,0?q=$latitude,$longitude&z=17&label=${
URLEncoder.encode(nodeName, "utf-8")
}",
)
withStyle(style = style) {
val gpsString = GPSFormat.toDec(latitude, longitude)
append(gpsString)
}
pop()
}
private fun handleClick(context: Context, annotatedString: AnnotatedString) {
annotatedString.getStringAnnotations(tag = "gps", start = 0, end = annotatedString.length).firstOrNull()?.let {
val uri = it.item.toUri()
val intent = Intent(Intent.ACTION_VIEW, uri).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
try {
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
Toast.makeText(context, "No application available to open this location!", Toast.LENGTH_LONG).show()
}
} catch (ex: ActivityNotFoundException) {
Timber.d("Failed to open geo intent: $ex")
}
}
}
@PreviewLightDark
@Composable
private fun LinkedCoordinatesPreview() {
AppTheme { LinkedCoordinates(latitude = 37.7749, longitude = -122.4194, nodeName = "Test Node Name") }
}

View file

@ -0,0 +1,88 @@
/*
* 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/>.
*/
package org.meshtastic.feature.node.component
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.Intent
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.core.net.toUri
import kotlinx.coroutines.launch
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.util.GPSFormat
import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.BasicListItem
import org.meshtastic.core.ui.component.icon
import org.meshtastic.core.ui.theme.AppTheme
import timber.log.Timber
import java.net.URLEncoder
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LinkedCoordinatesItem(node: Node) {
val context = LocalContext.current
val clipboard: Clipboard = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
val ago = formatAgo(node.position.time)
val coordinates = GPSFormat.toDec(node.latitude, node.longitude)
BasicListItem(
text = stringResource(R.string.last_position_update),
leadingIcon = Icons.Default.LocationOn,
supportingText = "$ago$coordinates",
trailingContent = Icons.AutoMirrored.Rounded.KeyboardArrowRight.icon(),
onClick = {
val label = URLEncoder.encode(node.user.longName, "utf-8")
val uri = "geo:0,0?q=${node.latitude},${node.longitude}&z=17&label=$label".toUri()
val intent = Intent(Intent.ACTION_VIEW, uri).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
try {
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
Toast.makeText(context, "No application available to open this location!", Toast.LENGTH_LONG).show()
}
} catch (ex: ActivityNotFoundException) {
Timber.d("Failed to open geo intent: $ex")
}
},
onLongClick = {
coroutineScope.launch { clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("", coordinates))) }
},
)
}
@PreviewLightDark
@Composable
private fun LinkedCoordinatesPreview() {
AppTheme { LinkedCoordinatesItem(Node(0)) }
}

View file

@ -25,7 +25,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.node.model.LogsType
import org.meshtastic.feature.node.model.MetricsState
@ -57,7 +57,7 @@ fun MetricsSection(
if (nonPositionLogs.isNotEmpty()) {
TitledCard(title = stringResource(id = R.string.logs), modifier = modifier) {
nonPositionLogs.forEach { type ->
SettingsItem(text = stringResource(type.titleRes), leadingIcon = type.icon) {
ListItem(text = stringResource(type.titleRes), leadingIcon = type.icon) {
onAction(NodeDetailAction.Navigate(type.route))
}
}

View file

@ -45,7 +45,7 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SettingsItemDetail
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
@ -80,48 +80,59 @@ fun NodeDetailsSection(node: Node, modifier: Modifier = Modifier) {
@Composable
private fun MainNodeDetails(node: Node) {
SettingsItemDetail(
ListItem(
text = stringResource(R.string.long_name),
icon = Icons.TwoTone.Person,
leadingIcon = Icons.TwoTone.Person,
supportingText = node.user.longName.ifEmpty { "???" },
copyable = true,
trailingIcon = null,
)
SettingsItemDetail(
ListItem(
text = stringResource(R.string.short_name),
icon = Icons.Outlined.Person,
leadingIcon = Icons.Outlined.Person,
supportingText = node.user.shortName.ifEmpty { "???" },
copyable = true,
trailingIcon = null,
)
SettingsItemDetail(
ListItem(
text = stringResource(R.string.node_number),
icon = Icons.Default.Numbers,
leadingIcon = Icons.Default.Numbers,
supportingText = node.num.toUInt().toString(),
copyable = true,
trailingIcon = null,
)
SettingsItemDetail(
ListItem(
text = stringResource(R.string.user_id),
icon = Icons.Default.Person,
leadingIcon = Icons.Default.Person,
supportingText = node.user.id,
copyable = true,
trailingIcon = null,
)
SettingsItemDetail(
ListItem(
text = stringResource(R.string.role),
icon = Icons.Default.Work,
leadingIcon = Icons.Default.Work,
supportingText = node.user.role.name,
trailingIcon = null,
)
if (node.isEffectivelyUnmessageable) {
SettingsItemDetail(
ListItem(
text = stringResource(R.string.unmonitored_or_infrastructure),
icon = Icons.Outlined.NoCell,
supportingText = null,
leadingIcon = Icons.Outlined.NoCell,
trailingIcon = null,
)
}
if (node.deviceMetrics.uptimeSeconds > 0) {
SettingsItemDetail(
ListItem(
text = stringResource(R.string.uptime),
icon = Icons.Default.CheckCircle,
leadingIcon = Icons.Default.CheckCircle,
supportingText = formatUptime(node.deviceMetrics.uptimeSeconds),
trailingIcon = null,
)
}
SettingsItemDetail(
ListItem(
text = stringResource(R.string.node_sort_last_heard),
icon = Icons.Default.History,
leadingIcon = Icons.Default.History,
supportingText = formatAgo(node.lastHeard),
trailingIcon = null,
)
}

View file

@ -17,26 +17,19 @@
package org.meshtastic.feature.node.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.SocialDistance
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.util.formatAgo
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.SettingsItemDetail
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.feature.node.model.LogsType
import org.meshtastic.feature.node.model.MetricsState
@ -61,52 +54,39 @@ fun PositionSection(
// Current position coordinates (linked)
if (hasValidPosition) {
InlineMap(node = node, Modifier.fillMaxWidth().height(200.dp))
SettingsItemDetail(
text = stringResource(R.string.last_position_update),
icon = Icons.Default.LocationOn,
supportingContent = {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(formatAgo(node.position.time), style = MaterialTheme.typography.titleLarge)
LinkedCoordinates(
latitude = node.latitude,
longitude = node.longitude,
nodeName = node.user.longName,
)
}
},
)
LinkedCoordinatesItem(node)
}
// Distance (if available)
if (distance != null && distance.isNotEmpty()) {
SettingsItemDetail(
ListItem(
text = stringResource(R.string.node_sort_distance),
icon = Icons.Default.SocialDistance,
leadingIcon = Icons.Default.SocialDistance,
supportingText = distance,
copyable = true,
trailingIcon = null,
)
}
// Exchange position action
SettingsItem(
ListItem(
text = stringResource(id = R.string.exchange_position),
leadingIcon = Icons.Default.LocationOn,
trailingContent = {},
trailingIcon = null,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestPosition(node))) },
)
// Node Map log
if (availableLogs.contains(LogsType.NODE_MAP)) {
SettingsItem(text = stringResource(LogsType.NODE_MAP.titleRes), leadingIcon = LogsType.NODE_MAP.icon) {
ListItem(text = stringResource(LogsType.NODE_MAP.titleRes), leadingIcon = LogsType.NODE_MAP.icon) {
onAction(NodeDetailAction.Navigate(LogsType.NODE_MAP.route))
}
}
// Positions Log
if (availableLogs.contains(LogsType.POSITIONS)) {
SettingsItem(text = stringResource(LogsType.POSITIONS.titleRes), leadingIcon = LogsType.POSITIONS.icon) {
ListItem(text = stringResource(LogsType.POSITIONS.titleRes), leadingIcon = LogsType.POSITIONS.icon) {
onAction(NodeDetailAction.Navigate(LogsType.POSITIONS.route))
}
}

View file

@ -24,24 +24,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.feature.node.model.NodeDetailAction
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
@Composable
internal fun RemoteDeviceActions(node: Node, lastTracerouteTime: Long?, onAction: (NodeDetailAction) -> Unit) {
if (!node.isEffectivelyUnmessageable) {
SettingsItem(
ListItem(
text = stringResource(id = R.string.direct_message),
leadingIcon = Icons.AutoMirrored.TwoTone.Message,
trailingContent = {},
trailingIcon = null,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.DirectMessage(node))) },
)
}
SettingsItem(
ListItem(
text = stringResource(id = R.string.exchange_userinfo),
leadingIcon = Icons.Default.Person,
trailingContent = {},
trailingIcon = null,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestUserInfo(node))) },
)
TracerouteButton(

View file

@ -35,7 +35,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.BasicListItem
import org.meshtastic.core.ui.theme.AppTheme
private const val COOL_DOWN_TIME_MS = 30000L
@ -70,7 +70,7 @@ private fun TracerouteButton(text: String, progress: Float, onClick: () -> Unit)
val stroke = Stroke(width = with(LocalDensity.current) { 2.dp.toPx() }, cap = StrokeCap.Round)
SettingsItem(
BasicListItem(
text = text,
enabled = !isCoolingDown,
leadingIcon = Icons.Default.Route,

View file

@ -34,6 +34,7 @@ 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.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.rounded.AppSettingsAlt
import androidx.compose.material.icons.rounded.FormatPaint
@ -52,6 +53,7 @@ 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.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
@ -63,11 +65,10 @@ import kotlinx.coroutines.delay
import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.MultipleChoiceAlertDialog
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.SettingsItemDetail
import org.meshtastic.core.ui.component.SettingsItemSwitch
import org.meshtastic.core.ui.component.SwitchListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
@ -230,11 +231,12 @@ fun SettingsScreen(
)
val context = LocalContext.current
val resources = LocalResources.current
TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
if (state.analyticsAvailable) {
val allowed by viewModel.analyticsAllowedFlow.collectAsStateWithLifecycle(false)
SettingsItemSwitch(
SwitchListItem(
text = stringResource(R.string.analytics_okay),
checked = allowed,
leadingIcon = Icons.Default.BugReport,
@ -255,7 +257,7 @@ fun SettingsScreen(
} else {
Toast.makeText(
context,
context.getString(R.string.location_disabled),
resources.getString(R.string.location_disabled),
Toast.LENGTH_LONG,
)
.show()
@ -269,14 +271,13 @@ fun SettingsScreen(
}
}
SettingsItemSwitch(
SwitchListItem(
text = stringResource(R.string.provide_location_to_mesh),
leadingIcon = Icons.Rounded.LocationOn,
enabled = !isGpsDisabled,
checked = provideLocation,
) {
settingsViewModel.setProvideLocation(!provideLocation)
}
onClick = { settingsViewModel.setProvideLocation(!provideLocation) },
)
val settingsLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {}
@ -284,15 +285,10 @@ fun SettingsScreen(
// On Android 12 and below, system app settings for language are not available. Use the in-app language
// picker for these devices.
val useInAppLangPicker = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
SettingsItem(
ListItem(
text = stringResource(R.string.preferences_language),
leadingIcon = Icons.Rounded.Language,
trailingContent =
if (useInAppLangPicker) {
null
} else {
{}
},
trailingIcon = if (useInAppLangPicker) null else Icons.AutoMirrored.Rounded.KeyboardArrowRight,
) {
if (useInAppLangPicker) {
showLanguagePickerDialog = true
@ -307,10 +303,10 @@ fun SettingsScreen(
}
}
SettingsItem(
ListItem(
text = stringResource(R.string.theme),
leadingIcon = Icons.Rounded.FormatPaint,
trailingContent = {},
trailingIcon = null,
) {
showThemePickerDialog = true
}
@ -322,10 +318,10 @@ fun SettingsScreen(
it.data?.data?.let { uri -> settingsViewModel.saveDataCsv(uri) }
}
}
SettingsItem(
ListItem(
text = stringResource(R.string.save_rangetest),
leadingIcon = Icons.Rounded.Output,
trailingContent = {},
trailingIcon = null,
) {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
@ -342,10 +338,10 @@ fun SettingsScreen(
it.data?.data?.let { uri -> settingsViewModel.saveDataCsv(uri) }
}
}
SettingsItem(
ListItem(
text = stringResource(R.string.export_data_csv),
leadingIcon = Icons.Rounded.Output,
trailingContent = {},
trailingIcon = null,
) {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
@ -356,18 +352,18 @@ fun SettingsScreen(
exportDataLauncher.launch(intent)
}
SettingsItem(
ListItem(
text = stringResource(R.string.intro_show),
leadingIcon = Icons.Rounded.WavingHand,
trailingContent = {},
trailingIcon = null,
) {
settingsViewModel.showAppIntro()
}
SettingsItem(
ListItem(
text = stringResource(R.string.system_settings),
leadingIcon = Icons.Rounded.AppSettingsAlt,
trailingContent = null,
trailingIcon = null,
) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", context.packageName, null)
@ -397,6 +393,7 @@ private fun AppVersionButton(
onUnlockExcludedModules: () -> Unit,
) {
val context = LocalContext.current
val resources = LocalResources.current
var clickCount by remember { mutableIntStateOf(0) }
LaunchedEffect(clickCount) {
@ -406,23 +403,25 @@ private fun AppVersionButton(
}
}
SettingsItemDetail(
ListItem(
text = stringResource(R.string.app_version),
icon = Icons.Rounded.Memory,
leadingIcon = Icons.Rounded.Memory,
supportingText = appVersionName,
trailingIcon = null,
) {
clickCount = clickCount.inc().coerceIn(0, UNLOCK_CLICK_COUNT)
when {
clickCount == UNLOCKED_CLICK_COUNT && excludedModulesUnlocked -> {
clickCount = 0
Toast.makeText(context, context.getString(R.string.modules_already_unlocked), Toast.LENGTH_LONG).show()
Toast.makeText(context, resources.getString(R.string.modules_already_unlocked), Toast.LENGTH_LONG)
.show()
}
clickCount == UNLOCK_CLICK_COUNT -> {
clickCount = 0
onUnlockExcludedModules()
Toast.makeText(context, context.getString(R.string.modules_unlocked), Toast.LENGTH_LONG).show()
Toast.makeText(context, resources.getString(R.string.modules_unlocked), Toast.LENGTH_LONG).show()
}
}
}
@ -448,13 +447,13 @@ private fun LanguagePickerDialog(onDismiss: () -> Unit) {
@Composable
private fun ThemePickerDialog(onClickTheme: (Int) -> Unit, onDismiss: () -> Unit) {
val context = LocalContext.current
val resources = LocalResources.current
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,
resources.getString(R.string.dynamic) to MODE_DYNAMIC,
resources.getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO,
resources.getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES,
resources.getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
)
.mapValues { (_, value) -> { onClickTheme(value) } }
}

View file

@ -45,7 +45,7 @@ import androidx.compose.ui.unit.dp
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.SettingsItem
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
@ -81,9 +81,7 @@ fun RadioConfigItemList(
ManagedMessage()
}
ConfigRoute.radioConfigRoutes.forEach {
SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) {
onRouteClick(it)
}
ListItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { onRouteClick(it) }
}
}
@ -92,9 +90,7 @@ fun RadioConfigItemList(
ManagedMessage()
}
ConfigRoute.deviceConfigRoutes(state.metadata).forEach {
SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) {
onRouteClick(it)
}
ListItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { onRouteClick(it) }
}
}
@ -104,9 +100,7 @@ fun RadioConfigItemList(
}
modules.forEach {
SettingsItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) {
onRouteClick(it)
}
ListItem(text = stringResource(it.title), leadingIcon = it.icon, enabled = enabled) { onRouteClick(it) }
}
}
}
@ -117,13 +111,13 @@ fun RadioConfigItemList(
ManagedMessage()
}
SettingsItem(
ListItem(
text = stringResource(R.string.import_configuration),
leadingIcon = Icons.Default.Download,
enabled = enabled,
onClick = onImport,
)
SettingsItem(
ListItem(
text = stringResource(R.string.export_configuration),
leadingIcon = Icons.Default.Upload,
enabled = enabled,
@ -143,11 +137,11 @@ fun RadioConfigItemList(
)
}
SettingsItem(
ListItem(
enabled = enabled,
text = stringResource(route.title),
leadingIcon = route.icon,
trailingContent = {},
trailingIcon = null,
) {
showDialog = true
}
@ -159,14 +153,14 @@ fun RadioConfigItemList(
ManagedMessage()
}
SettingsItem(
ListItem(
text = stringResource(R.string.clean_node_database_title),
leadingIcon = Icons.Rounded.CleaningServices,
enabled = enabled,
onClick = { onNavigate(SettingsRoutes.CleanNodeDb) },
)
SettingsItem(
ListItem(
text = stringResource(R.string.debug_panel),
leadingIcon = Icons.Rounded.BugReport,
enabled = enabled,