mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Clean up list item component API (#3465)
This commit is contained in:
parent
1b9f0f9736
commit
51ccc59b24
18 changed files with 407 additions and 476 deletions
|
|
@ -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),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ fun NodeDetailList(
|
|||
}
|
||||
|
||||
NodeDetailsSection(node)
|
||||
|
||||
NotesSection(node = node, onSaveNotes = onSaveNotes)
|
||||
|
||||
DeviceActions(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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") }
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
}
|
||||
|
|
@ -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)) }
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) } }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue