Merge branch 'main' into release/2.7.0

This commit is contained in:
James Rich 2025-09-04 22:41:06 -05:00 committed by GitHub
commit f287e35a69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 727 additions and 584 deletions

180
.github/renovate.json vendored
View file

@ -17,7 +17,10 @@
},
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"matchUpdateTypes": [
"minor",
"patch"
],
"matchCurrentVersion": "!/^0/",
"automerge": true
},
@ -37,153 +40,194 @@
},
{
"description": "Group all AndroidX dependencies (excluding more specific AndroidX groups)",
"matchPackagePatterns": ["^androidx\\."],
"excludePackagePatterns": [
"^androidx\\.room",
"^androidx\\.lifecycle",
"^androidx\\.navigation",
"^androidx\\.datastore",
"^androidx\\.compose\\.material3\\.adaptive",
"^androidx\\.compose\\.material3:material3-adaptive-navigation-suite$",
"^androidx\\.test\\.espresso",
"^androidx\\.test\\.ext",
"^androidx\\.compose\\.ui:ui-test-junit4$",
"^androidx\\.hilt"
],
"groupName": "AndroidX (General)",
"groupSlug": "androidx-general"
"groupSlug": "androidx-general",
"matchPackageNames": [
"/^androidx\\./",
"!/^androidx\\.room/",
"!/^androidx\\.lifecycle/",
"!/^androidx\\.navigation/",
"!/^androidx\\.datastore/",
"!/^androidx\\.compose\\.material3\\.adaptive/",
"!/^androidx\\.compose\\.material3:material3-adaptive-navigation-suite$/",
"!/^androidx\\.test\\.espresso/",
"!/^androidx\\.test\\.ext/",
"!/^androidx\\.compose\\.ui:ui-test-junit4$/",
"!/^androidx\\.hilt/"
]
},
{
"description": "Group Kotlin standard library, coroutines, and serialization",
"matchPackagePatterns": ["^org\\.jetbrains\\.kotlin", "^org\\.jetbrains\\.kotlinx"],
"groupName": "Kotlin Ecosystem",
"groupSlug": "kotlin"
"groupSlug": "kotlin",
"matchPackageNames": [
"/^org\\.jetbrains\\.kotlin/",
"/^org\\.jetbrains\\.kotlinx/"
]
},
{
"description": "Group Dagger and Hilt dependencies",
"matchPackagePatterns": ["^com\\.google\\.dagger", "^androidx\\.hilt"],
"groupName": "Dagger & Hilt",
"groupSlug": "hilt"
"groupSlug": "hilt",
"matchPackageNames": [
"/^com\\.google\\.dagger/",
"/^androidx\\.hilt/"
]
},
{
"description": "Group Accompanist libraries",
"matchPackagePatterns": ["^com\\.google\\.accompanist"],
"groupName": "Accompanist",
"groupSlug": "accompanist"
"groupSlug": "accompanist",
"matchPackageNames": [
"/^com\\.google\\.accompanist/"
]
},
{
"description": "Group JVM testing libraries (JUnit, Mockito, Robolectric)",
"matchPackagePatterns": [
"^junit:junit$",
"^org\\.mockito:",
"^org\\.robolectric:robolectric$"
],
"groupName": "JVM Testing Libraries",
"groupSlug": "jvm-testing"
"groupSlug": "jvm-testing",
"matchPackageNames": [
"/^junit:junit$/",
"/^org\\.mockito:/",
"/^org\\.robolectric:robolectric$/"
]
},
{
"description": "Group AndroidX Testing libraries",
"matchPackagePatterns": [
"^androidx\\.test\\.espresso",
"^androidx\\.test\\.ext",
"^androidx\\.compose\\.ui:ui-test-junit4$"
],
"groupName": "AndroidX Testing",
"groupSlug": "androidx-testing"
"groupSlug": "androidx-testing",
"matchPackageNames": [
"/^androidx\\.test\\.espresso/",
"/^androidx\\.test\\.ext/",
"/^androidx\\.compose\\.ui:ui-test-junit4$/"
]
},
{
"description": "Group Square networking libraries (OkHttp, Retrofit)",
"matchPackagePatterns": ["^com\\.squareup\\.okhttp3", "^com\\.squareup\\.retrofit2"],
"groupName": "Square Networking",
"groupSlug": "square-network"
"groupSlug": "square-network",
"matchPackageNames": [
"/^com\\.squareup\\.okhttp3/",
"/^com\\.squareup\\.retrofit2/"
]
},
{
"description": "Group Coil image loading library",
"matchPackagePatterns": ["^io\\.coil-kt\\.coil3"],
"groupName": "Coil",
"groupSlug": "coil"
"groupSlug": "coil",
"matchPackageNames": [
"/^io\\.coil-kt\\.coil3/"
]
},
{
"description": "Group ZXing barcode scanning libraries",
"matchPackagePatterns": ["^com\\.journeyapps:zxing-android-embedded", "^com\\.google\\.zxing:core"],
"groupName": "ZXing",
"groupSlug": "zxing"
"groupSlug": "zxing",
"matchPackageNames": [
"/^com\\.journeyapps:zxing-android-embedded/",
"/^com\\.google\\.zxing:core/"
]
},
{
"description": "Group Eclipse Paho MQTT client libraries",
"matchPackagePatterns": ["^org\\.eclipse\\.paho"],
"groupName": "MQTT Paho Client",
"groupSlug": "mqtt-paho"
"groupSlug": "mqtt-paho",
"matchPackageNames": [
"/^org\\.eclipse\\.paho/"
]
},
{
"description": "Group Mike Penz Markdown renderer libraries",
"matchPackagePatterns": ["^com\\.mikepenz"],
"groupName": "Markdown Renderer (Mike Penz)",
"groupSlug": "markdown-renderer-mikepenz"
"groupSlug": "markdown-renderer-mikepenz",
"matchPackageNames": [
"/^com\\.mikepenz/"
]
},
{
"description": "Group Firebase libraries",
"matchPackagePatterns": ["^com\\.google\\.firebase"],
"groupName": "Firebase",
"groupSlug": "firebase"
"groupSlug": "firebase",
"matchPackageNames": [
"/^com\\.google\\.firebase/"
]
},
{
"description": "Group Datadog libraries",
"matchPackagePatterns": ["^com\\.datadoghq"],
"groupName": "Datadog",
"groupSlug": "datadog"
"groupSlug": "datadog",
"matchPackageNames": [
"/^com\\.datadoghq/"
]
},
{
"description": "Group OpenStreetMap (OSM) libraries",
"matchPackagePatterns": ["^org\\.osmdroid", "^com\\.github\\.MKergall\\.osmbonuspack", "^mil\\.nga"],
"groupName": "OSM Libraries",
"groupSlug": "osm-libraries"
"groupSlug": "osm-libraries",
"matchPackageNames": [
"/^org\\.osmdroid/",
"/^com\\.github\\.MKergall\\.osmbonuspack/",
"/^mil\\.nga/"
]
},
{
"description": "Group Google Maps Compose libraries",
"matchPackagePatterns": ["^com\\.google\\.android\\.gms:play-services-location", "^com\\.google\\.maps\\.android"],
"groupName": "Google Maps Compose",
"groupSlug": "google-maps-compose"
"groupSlug": "google-maps-compose",
"matchPackageNames": [
"/^com\\.google\\.android\\.gms:play-services-location/",
"/^com\\.google\\.maps\\.android/"
]
},
{
"description": "Group Google Protobuf runtime libraries",
"matchPackagePatterns": ["^com\\.google\\.protobuf"],
"excludePackageNames": ["https://github.com/meshtastic/protobufs.git"],
"groupName": "Protobuf Runtime",
"groupSlug": "protobuf-runtime"
"groupSlug": "protobuf-runtime",
"matchPackageNames": [
"/^com\\.google\\.protobuf/",
"!https://github.com/meshtastic/protobufs.git"
]
},
{
"description": "Group AndroidX Room libraries",
"matchPackagePatterns": ["^androidx\\.room"],
"groupName": "AndroidX Room",
"groupSlug": "androidx-room"
"groupSlug": "androidx-room",
"matchPackageNames": [
"/^androidx\\.room/"
]
},
{
"description": "Group AndroidX Lifecycle libraries",
"matchPackagePatterns": ["^androidx\\.lifecycle"],
"groupName": "AndroidX Lifecycle",
"groupSlug": "androidx-lifecycle"
"groupSlug": "androidx-lifecycle",
"matchPackageNames": [
"/^androidx\\.lifecycle/"
]
},
{
"description": "Group AndroidX Navigation libraries",
"matchPackagePatterns": ["^androidx\\.navigation"],
"groupName": "AndroidX Navigation",
"groupSlug": "androidx-navigation"
"groupSlug": "androidx-navigation",
"matchPackageNames": [
"/^androidx\\.navigation/"
]
},
{
"description": "Group AndroidX DataStore libraries",
"matchPackagePatterns": ["^androidx\\.datastore"],
"groupName": "AndroidX DataStore",
"groupSlug": "androidx-datastore"
"groupSlug": "androidx-datastore",
"matchPackageNames": [
"/^androidx\\.datastore/"
]
},
{
"description": "Group AndroidX Adaptive UI libraries",
"matchPackagePatterns": [
"^androidx\\.compose\\.material3\\.adaptive",
"^androidx\\.compose\\.material3:material3-adaptive-navigation-suite$"
],
"groupName": "AndroidX Adaptive UI",
"groupSlug": "androidx-adaptive-ui"
"groupSlug": "androidx-adaptive-ui",
"matchPackageNames": [
"/^androidx\\.compose\\.material3\\.adaptive/",
"/^androidx\\.compose\\.material3:material3-adaptive-navigation-suite$/"
]
}
]
}
}

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check for PR labels
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const labels = context.payload.pull_request.labels.map(label => label.name);

View file

@ -12,4 +12,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: label-the-PR
uses: actions/labeler@v5
uses: actions/labeler@v6

View file

@ -114,24 +114,25 @@ android {
)
ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") }
}
flavorDimensions.add("default")
flavorDimensions += "default"
productFlavors {
// Read versionCode from defaultConfig after it's been potentially set by ENV or fallback
val resolvedVersionCode = defaultConfig.versionCode
val resolvedVersionName = defaultConfig.versionName
create("fdroid") {
dimension = "default"
dependenciesInfo { includeInApk = false }
versionName = "$resolvedVersionName ($resolvedVersionCode) fdroid"
}
create("google") {
dimension = "default"
isDefault = true
// Enable Firebase Crashlytics for Google Play builds
apply(plugin = libs.plugins.google.services.get().pluginId)
apply(plugin = libs.plugins.firebase.crashlytics.get().pluginId)
versionName = "$resolvedVersionName ($resolvedVersionCode) google"
}
create("fdroid") {
dimension = "default"
dependenciesInfo { includeInApk = false }
versionName = "$resolvedVersionName ($resolvedVersionCode) fdroid"
}
}
buildTypes {
release {

View file

@ -42,9 +42,9 @@ import com.datadog.android.sessionreplay.SessionReplay
import com.datadog.android.sessionreplay.SessionReplayConfiguration
import com.datadog.android.sessionreplay.compose.ComposeExtensionSupport
import com.datadog.android.timber.DatadogTree
import com.datadog.android.trace.AndroidTracer
import com.datadog.android.trace.Trace
import com.datadog.android.trace.TraceConfiguration
import com.datadog.android.trace.opentelemetry.DatadogOpenTelemetry
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.analytics.AnalyticsProvider
import com.geeksville.mesh.analytics.FirebaseAnalytics
@ -59,7 +59,7 @@ import com.google.firebase.crashlytics.crashlytics
import com.google.firebase.crashlytics.setCustomKeys
import com.google.firebase.initialize
import com.suddenh4x.ratingdialog.AppRating
import io.opentracing.util.GlobalTracer
import io.opentelemetry.api.GlobalOpenTelemetry
import timber.log.Timber
abstract class GeeksvilleApplication :
@ -218,8 +218,7 @@ abstract class GeeksvilleApplication :
val traceConfig = TraceConfiguration.Builder().build()
Trace.enable(traceConfig)
val tracer = AndroidTracer.Builder().build()
GlobalTracer.registerIfAbsent(tracer)
GlobalOpenTelemetry.set(DatadogOpenTelemetry(BuildConfig.APPLICATION_ID))
val sessionReplayConfig =
SessionReplayConfiguration.Builder(sampleRate = 20.0f)

View file

@ -27,29 +27,23 @@ import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.core.animate
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.TripOrigin
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingToolbarDefaults
import androidx.compose.material3.FloatingToolbarExitDirection.Companion.End
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.rememberFloatingToolbarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@ -62,7 +56,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -123,7 +116,7 @@ import com.google.maps.android.compose.Polyline
import com.google.maps.android.compose.TileOverlay
import com.google.maps.android.compose.rememberCameraPositionState
import com.google.maps.android.compose.rememberUpdatedMarkerState
import com.google.maps.android.compose.widgets.DisappearingScaleBar
import com.google.maps.android.compose.widgets.ScaleBar
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import timber.log.Timber
@ -214,7 +207,6 @@ fun MapView(
val mapFilterState by mapViewModel.mapFilterStateFlow.collectAsStateWithLifecycle()
val ourNodeInfo by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle()
var editingWaypoint by remember { mutableStateOf<Waypoint?>(null) }
val savedCameraPosition by mapViewModel.cameraPosition.collectAsStateWithLifecycle()
val selectedGoogleMapType by mapViewModel.selectedGoogleMapType.collectAsStateWithLifecycle()
val currentCustomTileProviderUrl by mapViewModel.selectedCustomTileProviderUrl.collectAsStateWithLifecycle()
@ -222,13 +214,7 @@ fun MapView(
var mapTypeMenuExpanded by remember { mutableStateOf(false) }
var showCustomTileManagerSheet by remember { mutableStateOf(false) }
val defaultLatLng = LatLng(0.0, 0.0)
val cameraPositionState = rememberCameraPositionState {
position =
savedCameraPosition?.let {
CameraPosition(LatLng(it.targetLat, it.targetLng), it.zoom, it.tilt, it.bearing)
} ?: CameraPosition.fromLatLngZoom(defaultLatLng, 7f)
}
val cameraPositionState = rememberCameraPositionState {}
// Location tracking functionality
val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
@ -277,29 +263,6 @@ fun MapView(
// Clean up location tracking on disposal
DisposableEffect(Unit) { onDispose { fusedLocationClient.removeLocationUpdates(locationCallback) } }
val floatingToolbarState = rememberFloatingToolbarState()
val exitAlwaysScrollBehavior =
FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = End, state = floatingToolbarState)
LaunchedEffect(cameraPositionState.isMoving, floatingToolbarState.offsetLimit) {
val targetOffset =
if (cameraPositionState.isMoving) {
floatingToolbarState.offsetLimit
} else {
mapViewModel.onCameraPositionChanged(cameraPositionState.position)
0f
}
if (floatingToolbarState.offset != targetOffset) {
if (targetOffset == 0f || floatingToolbarState.offsetLimit != 0f) {
launch {
animate(initialValue = floatingToolbarState.offset, targetValue = targetOffset) { value, _ ->
floatingToolbarState.offset = value
}
}
}
}
}
val allNodes by
mapViewModel.nodes
.map { nodes -> nodes.filter { node -> node.validPosition != null } }
@ -370,7 +333,7 @@ fun MapView(
var showClusterItemsDialog by remember { mutableStateOf<List<NodeClusterItem>?>(null) }
Scaffold(modifier = Modifier.nestedScroll(exitAlwaysScrollBehavior)) { paddingValues ->
Scaffold { paddingValues ->
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
GoogleMap(
mapColorScheme = mapColorScheme,
@ -380,7 +343,7 @@ fun MapView(
MapUiSettings(
zoomControlsEnabled = true,
mapToolbarEnabled = true,
compassEnabled = true,
compassEnabled = false,
myLocationButtonEnabled = false, // Disabled - we use custom location button
rotationGesturesEnabled = true,
scrollGesturesEnabled = true,
@ -399,32 +362,27 @@ fun MapView(
}
},
onMapLoaded = {
if (
savedCameraPosition?.targetLat == defaultLatLng.latitude &&
savedCameraPosition?.targetLng == defaultLatLng.longitude
) {
val pointsToBound: List<LatLng> =
when {
!nodeTrack.isNullOrEmpty() -> nodeTrack.map { it.toLatLng() }
val pointsToBound: List<LatLng> =
when {
!nodeTrack.isNullOrEmpty() -> nodeTrack.map { it.toLatLng() }
allNodes.isNotEmpty() || displayableWaypoints.isNotEmpty() ->
allNodes.mapNotNull { it.toLatLng() } + displayableWaypoints.map { it.toLatLng() }
allNodes.isNotEmpty() || displayableWaypoints.isNotEmpty() ->
allNodes.mapNotNull { it.toLatLng() } + displayableWaypoints.map { it.toLatLng() }
else -> emptyList()
}
if (pointsToBound.isNotEmpty()) {
val bounds = LatLngBounds.builder().apply { pointsToBound.forEach(::include) }.build()
val padding = if (!pointsToBound.isEmpty()) 100 else 48
try {
coroutineScope.launch {
cameraPositionState.animate(CameraUpdateFactory.newLatLngBounds(bounds, padding))
}
} catch (e: IllegalStateException) {
warn("MapView Could not animate to bounds: ${e.message}")
else -> emptyList()
}
if (pointsToBound.isNotEmpty()) {
val bounds = LatLngBounds.builder().apply { pointsToBound.forEach(::include) }.build()
val padding = if (!pointsToBound.isEmpty()) 100 else 48
try {
coroutineScope.launch {
cameraPositionState.animate(CameraUpdateFactory.newLatLngBounds(bounds, padding))
}
} catch (e: IllegalStateException) {
warn("MapView Could not animate to bounds: ${e.message}")
}
}
},
@ -550,6 +508,7 @@ fun MapView(
}
}
}
LayerType.GEOJSON -> {
layerItem.geoJsonLayerData?.let { geoJsonLayer ->
if (layerItem.isVisible && !geoJsonLayer.isLayerOnMap) {
@ -565,12 +524,10 @@ fun MapView(
}
}
val currentCameraPosition = cameraPositionState.position
var displayedZoom by remember { mutableStateOf(currentCameraPosition.zoom) }
if (displayedZoom != 0f) {
DisappearingScaleBar(cameraPositionState = cameraPositionState)
}
ScaleBar(
cameraPositionState = cameraPositionState,
modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 48.dp),
)
editingWaypoint?.let { waypointToEdit ->
EditWaypointDialog(
waypoint = waypointToEdit,
@ -599,7 +556,7 @@ fun MapView(
}
MapControlsOverlay(
modifier = Modifier.align(Alignment.TopEnd).padding(top = 50.dp),
modifier = Modifier.align(Alignment.TopCenter).padding(top = 8.dp),
mapFilterMenuExpanded = mapFilterMenuExpanded,
onMapFilterMenuDismissRequest = { mapFilterMenuExpanded = false },
onToggleMapFilterMenu = { mapFilterMenuExpanded = true },
@ -613,7 +570,6 @@ fun MapView(
showCustomTileManagerSheet = true
},
showFilterButton = focusedNodeNum == null,
scrollBehavior = exitAlwaysScrollBehavior,
hasLocationPermission = hasLocationPermission,
isLocationTrackingEnabled = isLocationTrackingEnabled,
onToggleLocationTracking = {
@ -642,6 +598,7 @@ fun MapView(
isLocationTrackingEnabled = !isLocationTrackingEnabled
}
},
bearing = cameraPositionState.position.bearing,
onOrientNorth = {
coroutineScope.launch {
try {

View file

@ -30,7 +30,6 @@ import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.map.CustomTileProviderRepository
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.TileProvider
import com.google.android.gms.maps.model.UrlTileProvider
import com.google.maps.android.compose.MapType
@ -101,10 +100,6 @@ constructor(
private val _selectedGoogleMapType = MutableStateFlow(MapType.NORMAL)
val selectedGoogleMapType: StateFlow<MapType> = _selectedGoogleMapType.asStateFlow()
private val _cameraPosition = MutableStateFlow<MapCameraPosition?>(null)
val cameraPosition: StateFlow<MapCameraPosition?> = _cameraPosition.asStateFlow()
val displayUnits =
radioConfigRepository.deviceProfileFlow
.mapNotNull { it.config.display.units }
@ -114,17 +109,6 @@ constructor(
initialValue = ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC,
)
fun onCameraPositionChanged(cameraPosition: CameraPosition) {
_cameraPosition.value =
MapCameraPosition(
targetLat = cameraPosition.target.latitude,
targetLng = cameraPosition.target.longitude,
zoom = cameraPosition.zoom,
tilt = cameraPosition.tilt,
bearing = cameraPosition.bearing,
)
}
fun addCustomTileProvider(name: String, urlTemplate: String) {
viewModelScope.launch {
if (name.isBlank() || urlTemplate.isBlank() || !isValidTileUrlTemplate(urlTemplate)) {

View file

@ -19,13 +19,25 @@ package com.geeksville.mesh.ui.map.components
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
@Composable
fun MapButton(icon: ImageVector, contentDescription: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
fun MapButton(
modifier: Modifier = Modifier,
icon: ImageVector,
iconTint: Color? = null,
contentDescription: String,
onClick: () -> Unit,
) {
FilledIconButton(onClick = onClick, modifier = modifier) {
Icon(imageVector = icon, contentDescription = contentDescription)
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = iconTint ?: IconButtonDefaults.filledIconButtonColors().contentColor,
)
}
}

View file

@ -20,18 +20,20 @@ package com.geeksville.mesh.ui.map.components
import androidx.compose.foundation.layout.Box
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationDisabled
import androidx.compose.material.icons.outlined.Explore
import androidx.compose.material.icons.outlined.Layers
import androidx.compose.material.icons.outlined.Map
import androidx.compose.material.icons.outlined.MyLocation
import androidx.compose.material.icons.outlined.Navigation
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingToolbarScrollBehavior
import androidx.compose.material3.VerticalFloatingToolbar
import androidx.compose.material3.HorizontalFloatingToolbar
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.stringResource
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.map.MapViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@ -48,20 +50,20 @@ fun MapControlsOverlay(
onManageLayersClicked: () -> Unit,
onManageCustomTileProvidersClicked: () -> Unit, // New parameter
showFilterButton: Boolean,
scrollBehavior: FloatingToolbarScrollBehavior,
// Location tracking parameters
hasLocationPermission: Boolean = false,
isLocationTrackingEnabled: Boolean = false,
onToggleLocationTracking: () -> Unit = {},
bearing: Float = 0f,
onOrientNorth: () -> Unit = {},
) {
VerticalFloatingToolbar(
HorizontalFloatingToolbar(
modifier = modifier,
expanded = true,
leadingContent = {},
trailingContent = {},
scrollBehavior = scrollBehavior,
content = {
CompassButton(onOrientNorth = onOrientNorth, bearing = bearing)
if (showFilterButton) {
Box {
MapButton(
@ -97,8 +99,6 @@ fun MapControlsOverlay(
onClick = onManageLayersClicked,
)
CompassButton(onOrientNorth = onOrientNorth)
// Location tracking button
if (hasLocationPermission) {
MapButton(
@ -117,9 +117,13 @@ fun MapControlsOverlay(
}
@Composable
private fun CompassButton(onOrientNorth: () -> Unit) {
private fun CompassButton(onOrientNorth: () -> Unit, bearing: Float) {
val icon = Icons.Outlined.Navigation
MapButton(
icon = Icons.Outlined.Explore,
modifier = Modifier.rotate(-bearing),
icon = icon,
iconTint = MaterialTheme.colorScheme.StatusRed.takeIf { bearing == 0f },
contentDescription = stringResource(id = R.string.orient_north),
onClick = onOrientNorth,
)

View file

@ -18,39 +18,39 @@
package com.geeksville.mesh.navigation
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.PowerSettingsNew
import androidx.compose.material.icons.rounded.RestartAlt
import androidx.compose.material.icons.rounded.Restore
import androidx.compose.material.icons.rounded.Storage
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navDeepLink
import com.geeksville.mesh.R
import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
import com.geeksville.mesh.ui.debug.DebugScreen
import com.geeksville.mesh.ui.map.MapViewModel
import kotlinx.serialization.Serializable
enum class AdminRoute(@StringRes val title: Int) {
REBOOT(R.string.reboot),
SHUTDOWN(R.string.shutdown),
FACTORY_RESET(R.string.factory_reset),
NODEDB_RESET(R.string.nodedb_reset),
enum class AdminRoute(val icon: ImageVector, @StringRes val title: Int) {
REBOOT(Icons.Rounded.RestartAlt, R.string.reboot),
SHUTDOWN(Icons.Rounded.PowerSettingsNew, R.string.shutdown),
FACTORY_RESET(Icons.Rounded.Restore, R.string.factory_reset),
NODEDB_RESET(Icons.Rounded.Storage, R.string.nodedb_reset),
}
const val DEEP_LINK_BASE_URI = "meshtastic://meshtastic"
@Serializable sealed interface Graph : Route
@Serializable
sealed interface Route {
@Serializable data object DebugPanel : Route
}
@Serializable sealed interface Route
fun NavDestination.isConfigRoute(): Boolean =
ConfigRoute.entries.any { hasRoute(it.route::class) } || ModuleRoute.entries.any { hasRoute(it.route::class) }
@ -80,11 +80,6 @@ fun NavGraph(
mapGraph(navController, uIViewModel, mapViewModel)
channelsGraph(navController, uIViewModel)
connectionsGraph(navController, uIViewModel, bluetoothViewModel)
composable<Route.DebugPanel>(
deepLinks = listOf(navDeepLink<Route.DebugPanel>(basePath = "$DEEP_LINK_BASE_URI/debug_panel")),
) {
DebugScreen()
}
settingsGraph(navController, uIViewModel)
}
}

View file

@ -56,6 +56,7 @@ import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.MeshProtos.DeviceMetadata
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.debug.DebugScreen
import com.geeksville.mesh.ui.settings.SettingsScreen
import com.geeksville.mesh.ui.settings.radio.CleanNodeDatabaseScreen
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
@ -147,6 +148,8 @@ sealed class SettingsRoutes {
@Serializable data object CleanNodeDb : Route
@Serializable data object DebugPanel : Route
// endregion
}
@ -177,6 +180,12 @@ fun NavGraphBuilder.settingsGraph(navController: NavHostController, uiViewModel:
}
configRoutesScreens(navController)
moduleRoutesScreens(navController)
composable<SettingsRoutes.DebugPanel>(
deepLinks =
listOf(navDeepLink<SettingsRoutes.DebugPanel>(basePath = "$DEEP_LINK_BASE_URI/settings/debug_panel")),
) {
DebugScreen()
}
}
}

View file

@ -199,7 +199,7 @@ class MeshService :
private const val CONFIG_ONLY_NONCE = 69420
private const val NODE_INFO_ONLY_NONCE = 69421
private const val CONFIG_WAIT_MS = 250L
private const val CONFIG_WAIT_MS = 50L
}
private var previousSummary: String? = null
@ -1750,9 +1750,6 @@ class MeshService :
processQueuedPackets() // send any packets that were queued up
startMqttClientProxy()
serviceBroadcasts.broadcastConnection()
packetHandler.sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { setTimeOnly = currentSecond() }) {
connectionState
}
sendAnalytics()
reportConnection()
}
@ -1825,6 +1822,9 @@ class MeshService :
sendAnalytics()
onNodeDBChanged()
}
packetHandler.sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { setTimeOnly = currentSecond() }) {
connectionState
}
}
/** Start the modern (REV2) API configuration flow */

View file

@ -351,7 +351,6 @@ fun MainScreen(
onAction = { action ->
if (action is MainMenuAction) {
when (action) {
MainMenuAction.DEBUG -> navController.navigate(Route.DebugPanel)
MainMenuAction.QUICK_CHAT -> navController.navigate(ContactsRoutes.QuickChat)
MainMenuAction.SHOW_INTRO -> uIViewModel.onMainMenuAction(action)
else -> onAction(action)

View file

@ -55,7 +55,6 @@ import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.ContactsRoutes
import com.geeksville.mesh.navigation.NodesRoutes
import com.geeksville.mesh.navigation.Route
import com.geeksville.mesh.navigation.SettingsRoutes
import com.geeksville.mesh.navigation.showLongNameTitle
import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
@ -88,7 +87,7 @@ fun MainAppBar(
when {
currentDestination == null || currentDestination.isTopLevel() -> stringResource(id = R.string.app_name)
currentDestination.hasRoute<Route.DebugPanel>() -> stringResource(id = R.string.debug_panel)
currentDestination.hasRoute<SettingsRoutes.DebugPanel>() -> stringResource(id = R.string.debug_panel)
currentDestination.hasRoute<ContactsRoutes.QuickChat>() -> stringResource(id = R.string.quick_chat)
@ -120,7 +119,7 @@ fun MainAppBar(
when {
it.isTopLevel() -> MainMenuActions(onAction)
currentDestination.hasRoute<Route.DebugPanel>() -> DebugMenuActions()
currentDestination.hasRoute<SettingsRoutes.DebugPanel>() -> DebugMenuActions()
currentDestination.hasRoute<SettingsRoutes.Settings>() ->
RadioConfigMenuActions(viewModel = viewModel)
@ -206,7 +205,6 @@ private fun TopBarActions(
}
enum class MainMenuAction(@StringRes val stringRes: Int) {
DEBUG(R.string.debug_panel),
EXPORT_RANGETEST(R.string.save_rangetest),
SHOW_INTRO(R.string.intro_show),
QUICK_CHAT(R.string.quick_chat),

View file

@ -23,6 +23,7 @@ import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.MotionScheme.Companion.expressive
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
@ -32,267 +33,270 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
private val lightScheme = lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
private val lightScheme =
lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
private val darkScheme = darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
primaryContainer = primaryContainerDark,
onPrimaryContainer = onPrimaryContainerDark,
secondary = secondaryDark,
onSecondary = onSecondaryDark,
secondaryContainer = secondaryContainerDark,
onSecondaryContainer = onSecondaryContainerDark,
tertiary = tertiaryDark,
onTertiary = onTertiaryDark,
tertiaryContainer = tertiaryContainerDark,
onTertiaryContainer = onTertiaryContainerDark,
error = errorDark,
onError = onErrorDark,
errorContainer = errorContainerDark,
onErrorContainer = onErrorContainerDark,
background = backgroundDark,
onBackground = onBackgroundDark,
surface = surfaceDark,
onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark,
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
surfaceDim = surfaceDimDark,
surfaceBright = surfaceBrightDark,
surfaceContainerLowest = surfaceContainerLowestDark,
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
)
private val darkScheme =
darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
primaryContainer = primaryContainerDark,
onPrimaryContainer = onPrimaryContainerDark,
secondary = secondaryDark,
onSecondary = onSecondaryDark,
secondaryContainer = secondaryContainerDark,
onSecondaryContainer = onSecondaryContainerDark,
tertiary = tertiaryDark,
onTertiary = onTertiaryDark,
tertiaryContainer = tertiaryContainerDark,
onTertiaryContainer = onTertiaryContainerDark,
error = errorDark,
onError = onErrorDark,
errorContainer = errorContainerDark,
onErrorContainer = onErrorContainerDark,
background = backgroundDark,
onBackground = onBackgroundDark,
surface = surfaceDark,
onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark,
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
surfaceDim = surfaceDimDark,
surfaceBright = surfaceBrightDark,
surfaceContainerLowest = surfaceContainerLowestDark,
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
)
private val mediumContrastLightColorScheme = lightColorScheme(
primary = primaryLightMediumContrast,
onPrimary = onPrimaryLightMediumContrast,
primaryContainer = primaryContainerLightMediumContrast,
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
secondary = secondaryLightMediumContrast,
onSecondary = onSecondaryLightMediumContrast,
secondaryContainer = secondaryContainerLightMediumContrast,
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
tertiary = tertiaryLightMediumContrast,
onTertiary = onTertiaryLightMediumContrast,
tertiaryContainer = tertiaryContainerLightMediumContrast,
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
error = errorLightMediumContrast,
onError = onErrorLightMediumContrast,
errorContainer = errorContainerLightMediumContrast,
onErrorContainer = onErrorContainerLightMediumContrast,
background = backgroundLightMediumContrast,
onBackground = onBackgroundLightMediumContrast,
surface = surfaceLightMediumContrast,
onSurface = onSurfaceLightMediumContrast,
surfaceVariant = surfaceVariantLightMediumContrast,
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
outline = outlineLightMediumContrast,
outlineVariant = outlineVariantLightMediumContrast,
scrim = scrimLightMediumContrast,
inverseSurface = inverseSurfaceLightMediumContrast,
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
inversePrimary = inversePrimaryLightMediumContrast,
surfaceDim = surfaceDimLightMediumContrast,
surfaceBright = surfaceBrightLightMediumContrast,
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
surfaceContainer = surfaceContainerLightMediumContrast,
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
)
private val mediumContrastLightColorScheme =
lightColorScheme(
primary = primaryLightMediumContrast,
onPrimary = onPrimaryLightMediumContrast,
primaryContainer = primaryContainerLightMediumContrast,
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
secondary = secondaryLightMediumContrast,
onSecondary = onSecondaryLightMediumContrast,
secondaryContainer = secondaryContainerLightMediumContrast,
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
tertiary = tertiaryLightMediumContrast,
onTertiary = onTertiaryLightMediumContrast,
tertiaryContainer = tertiaryContainerLightMediumContrast,
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
error = errorLightMediumContrast,
onError = onErrorLightMediumContrast,
errorContainer = errorContainerLightMediumContrast,
onErrorContainer = onErrorContainerLightMediumContrast,
background = backgroundLightMediumContrast,
onBackground = onBackgroundLightMediumContrast,
surface = surfaceLightMediumContrast,
onSurface = onSurfaceLightMediumContrast,
surfaceVariant = surfaceVariantLightMediumContrast,
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
outline = outlineLightMediumContrast,
outlineVariant = outlineVariantLightMediumContrast,
scrim = scrimLightMediumContrast,
inverseSurface = inverseSurfaceLightMediumContrast,
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
inversePrimary = inversePrimaryLightMediumContrast,
surfaceDim = surfaceDimLightMediumContrast,
surfaceBright = surfaceBrightLightMediumContrast,
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
surfaceContainer = surfaceContainerLightMediumContrast,
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
)
private val highContrastLightColorScheme = lightColorScheme(
primary = primaryLightHighContrast,
onPrimary = onPrimaryLightHighContrast,
primaryContainer = primaryContainerLightHighContrast,
onPrimaryContainer = onPrimaryContainerLightHighContrast,
secondary = secondaryLightHighContrast,
onSecondary = onSecondaryLightHighContrast,
secondaryContainer = secondaryContainerLightHighContrast,
onSecondaryContainer = onSecondaryContainerLightHighContrast,
tertiary = tertiaryLightHighContrast,
onTertiary = onTertiaryLightHighContrast,
tertiaryContainer = tertiaryContainerLightHighContrast,
onTertiaryContainer = onTertiaryContainerLightHighContrast,
error = errorLightHighContrast,
onError = onErrorLightHighContrast,
errorContainer = errorContainerLightHighContrast,
onErrorContainer = onErrorContainerLightHighContrast,
background = backgroundLightHighContrast,
onBackground = onBackgroundLightHighContrast,
surface = surfaceLightHighContrast,
onSurface = onSurfaceLightHighContrast,
surfaceVariant = surfaceVariantLightHighContrast,
onSurfaceVariant = onSurfaceVariantLightHighContrast,
outline = outlineLightHighContrast,
outlineVariant = outlineVariantLightHighContrast,
scrim = scrimLightHighContrast,
inverseSurface = inverseSurfaceLightHighContrast,
inverseOnSurface = inverseOnSurfaceLightHighContrast,
inversePrimary = inversePrimaryLightHighContrast,
surfaceDim = surfaceDimLightHighContrast,
surfaceBright = surfaceBrightLightHighContrast,
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
surfaceContainerLow = surfaceContainerLowLightHighContrast,
surfaceContainer = surfaceContainerLightHighContrast,
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
)
private val highContrastLightColorScheme =
lightColorScheme(
primary = primaryLightHighContrast,
onPrimary = onPrimaryLightHighContrast,
primaryContainer = primaryContainerLightHighContrast,
onPrimaryContainer = onPrimaryContainerLightHighContrast,
secondary = secondaryLightHighContrast,
onSecondary = onSecondaryLightHighContrast,
secondaryContainer = secondaryContainerLightHighContrast,
onSecondaryContainer = onSecondaryContainerLightHighContrast,
tertiary = tertiaryLightHighContrast,
onTertiary = onTertiaryLightHighContrast,
tertiaryContainer = tertiaryContainerLightHighContrast,
onTertiaryContainer = onTertiaryContainerLightHighContrast,
error = errorLightHighContrast,
onError = onErrorLightHighContrast,
errorContainer = errorContainerLightHighContrast,
onErrorContainer = onErrorContainerLightHighContrast,
background = backgroundLightHighContrast,
onBackground = onBackgroundLightHighContrast,
surface = surfaceLightHighContrast,
onSurface = onSurfaceLightHighContrast,
surfaceVariant = surfaceVariantLightHighContrast,
onSurfaceVariant = onSurfaceVariantLightHighContrast,
outline = outlineLightHighContrast,
outlineVariant = outlineVariantLightHighContrast,
scrim = scrimLightHighContrast,
inverseSurface = inverseSurfaceLightHighContrast,
inverseOnSurface = inverseOnSurfaceLightHighContrast,
inversePrimary = inversePrimaryLightHighContrast,
surfaceDim = surfaceDimLightHighContrast,
surfaceBright = surfaceBrightLightHighContrast,
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
surfaceContainerLow = surfaceContainerLowLightHighContrast,
surfaceContainer = surfaceContainerLightHighContrast,
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
)
private val mediumContrastDarkColorScheme = darkColorScheme(
primary = primaryDarkMediumContrast,
onPrimary = onPrimaryDarkMediumContrast,
primaryContainer = primaryContainerDarkMediumContrast,
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
secondary = secondaryDarkMediumContrast,
onSecondary = onSecondaryDarkMediumContrast,
secondaryContainer = secondaryContainerDarkMediumContrast,
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
tertiary = tertiaryDarkMediumContrast,
onTertiary = onTertiaryDarkMediumContrast,
tertiaryContainer = tertiaryContainerDarkMediumContrast,
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
error = errorDarkMediumContrast,
onError = onErrorDarkMediumContrast,
errorContainer = errorContainerDarkMediumContrast,
onErrorContainer = onErrorContainerDarkMediumContrast,
background = backgroundDarkMediumContrast,
onBackground = onBackgroundDarkMediumContrast,
surface = surfaceDarkMediumContrast,
onSurface = onSurfaceDarkMediumContrast,
surfaceVariant = surfaceVariantDarkMediumContrast,
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
outline = outlineDarkMediumContrast,
outlineVariant = outlineVariantDarkMediumContrast,
scrim = scrimDarkMediumContrast,
inverseSurface = inverseSurfaceDarkMediumContrast,
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
inversePrimary = inversePrimaryDarkMediumContrast,
surfaceDim = surfaceDimDarkMediumContrast,
surfaceBright = surfaceBrightDarkMediumContrast,
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
surfaceContainer = surfaceContainerDarkMediumContrast,
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
)
private val mediumContrastDarkColorScheme =
darkColorScheme(
primary = primaryDarkMediumContrast,
onPrimary = onPrimaryDarkMediumContrast,
primaryContainer = primaryContainerDarkMediumContrast,
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
secondary = secondaryDarkMediumContrast,
onSecondary = onSecondaryDarkMediumContrast,
secondaryContainer = secondaryContainerDarkMediumContrast,
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
tertiary = tertiaryDarkMediumContrast,
onTertiary = onTertiaryDarkMediumContrast,
tertiaryContainer = tertiaryContainerDarkMediumContrast,
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
error = errorDarkMediumContrast,
onError = onErrorDarkMediumContrast,
errorContainer = errorContainerDarkMediumContrast,
onErrorContainer = onErrorContainerDarkMediumContrast,
background = backgroundDarkMediumContrast,
onBackground = onBackgroundDarkMediumContrast,
surface = surfaceDarkMediumContrast,
onSurface = onSurfaceDarkMediumContrast,
surfaceVariant = surfaceVariantDarkMediumContrast,
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
outline = outlineDarkMediumContrast,
outlineVariant = outlineVariantDarkMediumContrast,
scrim = scrimDarkMediumContrast,
inverseSurface = inverseSurfaceDarkMediumContrast,
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
inversePrimary = inversePrimaryDarkMediumContrast,
surfaceDim = surfaceDimDarkMediumContrast,
surfaceBright = surfaceBrightDarkMediumContrast,
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
surfaceContainer = surfaceContainerDarkMediumContrast,
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
)
private val highContrastDarkColorScheme = darkColorScheme(
primary = primaryDarkHighContrast,
onPrimary = onPrimaryDarkHighContrast,
primaryContainer = primaryContainerDarkHighContrast,
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
secondary = secondaryDarkHighContrast,
onSecondary = onSecondaryDarkHighContrast,
secondaryContainer = secondaryContainerDarkHighContrast,
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
tertiary = tertiaryDarkHighContrast,
onTertiary = onTertiaryDarkHighContrast,
tertiaryContainer = tertiaryContainerDarkHighContrast,
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
error = errorDarkHighContrast,
onError = onErrorDarkHighContrast,
errorContainer = errorContainerDarkHighContrast,
onErrorContainer = onErrorContainerDarkHighContrast,
background = backgroundDarkHighContrast,
onBackground = onBackgroundDarkHighContrast,
surface = surfaceDarkHighContrast,
onSurface = onSurfaceDarkHighContrast,
surfaceVariant = surfaceVariantDarkHighContrast,
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
outline = outlineDarkHighContrast,
outlineVariant = outlineVariantDarkHighContrast,
scrim = scrimDarkHighContrast,
inverseSurface = inverseSurfaceDarkHighContrast,
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
inversePrimary = inversePrimaryDarkHighContrast,
surfaceDim = surfaceDimDarkHighContrast,
surfaceBright = surfaceBrightDarkHighContrast,
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
surfaceContainer = surfaceContainerDarkHighContrast,
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
)
private val highContrastDarkColorScheme =
darkColorScheme(
primary = primaryDarkHighContrast,
onPrimary = onPrimaryDarkHighContrast,
primaryContainer = primaryContainerDarkHighContrast,
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
secondary = secondaryDarkHighContrast,
onSecondary = onSecondaryDarkHighContrast,
secondaryContainer = secondaryContainerDarkHighContrast,
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
tertiary = tertiaryDarkHighContrast,
onTertiary = onTertiaryDarkHighContrast,
tertiaryContainer = tertiaryContainerDarkHighContrast,
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
error = errorDarkHighContrast,
onError = onErrorDarkHighContrast,
errorContainer = errorContainerDarkHighContrast,
onErrorContainer = onErrorContainerDarkHighContrast,
background = backgroundDarkHighContrast,
onBackground = onBackgroundDarkHighContrast,
surface = surfaceDarkHighContrast,
onSurface = onSurfaceDarkHighContrast,
surfaceVariant = surfaceVariantDarkHighContrast,
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
outline = outlineDarkHighContrast,
outlineVariant = outlineVariantDarkHighContrast,
scrim = scrimDarkHighContrast,
inverseSurface = inverseSurfaceDarkHighContrast,
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
inversePrimary = inversePrimaryDarkHighContrast,
surfaceDim = surfaceDimDarkHighContrast,
surfaceBright = surfaceBrightDarkHighContrast,
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
surfaceContainer = surfaceContainerDarkHighContrast,
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
)
@Immutable
data class ColorFamily(
val color: Color,
val onColor: Color,
val colorContainer: Color,
val onColorContainer: Color
)
data class ColorFamily(val color: Color, val onColor: Color, val colorContainer: Color, val onColorContainer: Color)
val unspecified_scheme = ColorFamily(
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
)
val unspecified_scheme = ColorFamily(Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable() () -> Unit
content:
@Composable()
() -> Unit,
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
val colorScheme =
when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> darkScheme
else -> lightScheme
}
darkTheme -> darkScheme
else -> lightScheme
}
MaterialExpressiveTheme(
colorScheme = colorScheme,
motionScheme = expressive(),
typography = AppTypography,
content = content
content = content,
)
}

View file

@ -20,8 +20,6 @@ package com.geeksville.mesh.ui.node
import android.content.Intent
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -93,11 +91,9 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -150,6 +146,7 @@ import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
import com.geeksville.mesh.ui.node.components.NodeActionDialogs
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.node.components.TracerouteButton
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
@ -631,8 +628,7 @@ private fun RemoteDeviceActions(node: Node, lastTracerouteTime: Long?, onAction:
trailingIcon = null,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.RequestUserInfo(node))) },
)
TracerouteActionButton(
title = stringResource(id = R.string.traceroute),
TracerouteButton(
lastTracerouteTime = lastTracerouteTime,
onClick = { onAction(NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.TraceRoute(node))) },
)
@ -1035,59 +1031,6 @@ private fun PowerMetrics(node: Node) {
}
}
private const val COOL_DOWN_TIME_MS = 30000L
@Composable
fun TracerouteActionButton(title: String, lastTracerouteTime: Long?, onClick: () -> Unit) {
val progress = remember { Animatable(0f) }
var isCoolingDown by remember { mutableStateOf(false) }
LaunchedEffect(lastTracerouteTime) {
val timeSinceLast = System.currentTimeMillis() - (lastTracerouteTime ?: 0)
isCoolingDown = timeSinceLast < COOL_DOWN_TIME_MS
if (isCoolingDown) {
val remainingTime = COOL_DOWN_TIME_MS - timeSinceLast
progress.snapTo(remainingTime / COOL_DOWN_TIME_MS.toFloat())
progress.animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = remainingTime.toInt(), easing = { it }),
)
isCoolingDown = false
}
}
Button(
onClick = {
if (!isCoolingDown) {
onClick()
}
},
enabled = !isCoolingDown,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).height(48.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (isCoolingDown) {
CircularProgressIndicator(
progress = { progress.value },
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp,
trackColor = ProgressIndicatorDefaults.circularDeterminateTrackColor,
strokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap,
)
} else {
Icon(
imageVector = Icons.Default.Route,
contentDescription = stringResource(R.string.traceroute),
modifier = Modifier.size(24.dp),
)
}
Spacer(modifier = Modifier.width(8.dp))
Text(text = title, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
}
}
}
@Composable
fun NodeActionButton(
modifier: Modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).height(48.dp),

View file

@ -0,0 +1,100 @@
/*
* 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 com.geeksville.mesh.ui.node.components
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Route
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.settings.components.SettingsItem
private const val COOL_DOWN_TIME_MS = 30000L
@Composable
fun TracerouteButton(
text: String = stringResource(id = R.string.traceroute),
lastTracerouteTime: Long?,
onClick: () -> Unit,
) {
val progress = remember { Animatable(0f) }
LaunchedEffect(lastTracerouteTime) {
val timeSinceLast = System.currentTimeMillis() - (lastTracerouteTime ?: 0)
if (timeSinceLast < COOL_DOWN_TIME_MS) {
val remainingTime = COOL_DOWN_TIME_MS - timeSinceLast
progress.snapTo(remainingTime / COOL_DOWN_TIME_MS.toFloat())
progress.animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = remainingTime.toInt(), easing = { it }),
)
}
}
TracerouteButton(text = text, progress = progress.value, onClick = onClick)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun TracerouteButton(text: String, progress: Float, onClick: () -> Unit) {
val isCoolingDown = progress > 0f
val stroke = Stroke(width = with(LocalDensity.current) { 2.dp.toPx() }, cap = StrokeCap.Round)
SettingsItem(
text = text,
enabled = !isCoolingDown,
leadingIcon = Icons.Default.Route,
trailingContent = {
if (isCoolingDown) {
CircularWavyProgressIndicator(
progress = { progress },
modifier = Modifier.size(24.dp),
stroke = stroke,
trackStroke = stroke,
wavelength = 8.dp,
)
}
},
onClick = {
if (!isCoolingDown) {
onClick()
}
},
)
}
@Preview(showBackground = true)
@Composable
private fun TracerouteButtonPreview() {
AppTheme { TracerouteButton(text = "Traceroute", progress = .6f, onClick = {}) }
}

View file

@ -26,7 +26,6 @@ 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.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
@ -45,28 +44,52 @@ import com.geeksville.mesh.ui.common.theme.AppTheme
@Composable
fun SettingsItem(
text: String,
textColor: Color = LocalContentColor.current,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
leadingIconTint: Color = LocalContentColor.current,
trailingIcon: ImageVector? = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
trailingIconTint: Color = LocalContentColor.current,
onClick: () -> Unit,
) {
SettingsItem(
text = text,
textColor = textColor,
enabled = enabled,
leadingIcon = leadingIcon,
leadingIconTint = leadingIconTint,
trailingContent = { trailingIcon.Icon(trailingIconTint) },
onClick = onClick,
)
}
/** A clickable settings button item. */
@Composable
fun SettingsItem(
text: String,
textColor: Color = LocalContentColor.current,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
leadingIconTint: Color = LocalContentColor.current,
trailingContent: @Composable (() -> Unit),
onClick: () -> Unit,
) {
ClickableWrapper(enabled = enabled, onClick = onClick) {
Content(
leading = { leadingIcon.Icon(leadingIconTint) },
text = text,
trailing = { trailingIcon.Icon(trailingIconTint) },
textColor = textColor,
trailing = trailingContent,
)
}
}
/** A toggleable settings switch item. */
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SettingsItemSwitch(
checked: Boolean,
text: String,
textColor: Color = LocalContentColor.current,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
leadingIconTint: Color = LocalContentColor.current,
@ -76,6 +99,7 @@ fun SettingsItemSwitch(
Content(
leading = { leadingIcon.Icon(leadingIconTint) },
text = text,
textColor = textColor,
trailing = { Switch(checked = checked, enabled = enabled, onCheckedChange = null) },
)
}
@ -85,6 +109,7 @@ fun SettingsItemSwitch(
@Composable
fun SettingsItemDetail(
text: String,
textColor: Color = LocalContentColor.current,
icon: ImageVector? = null,
iconTint: Color = LocalContentColor.current,
trailingText: String? = null,
@ -92,7 +117,12 @@ fun SettingsItemDetail(
onClick: (() -> Unit)? = null,
) {
val content: @Composable ColumnScope.() -> Unit = {
Content(leading = { icon.Icon(iconTint) }, text = text, trailing = { trailingText?.let { Text(text = it) } })
Content(
leading = { icon.Icon(iconTint) },
text = text,
textColor = textColor,
trailing = { trailingText?.let { Text(text = it) } },
)
}
if (onClick != null) {
@ -116,11 +146,11 @@ private fun ClickableWrapper(enabled: Boolean, onClick: () -> Unit, content: @Co
/** The row content to display for a settings item. */
@Composable
private fun Content(leading: @Composable () -> Unit, text: String, trailing: @Composable () -> Unit) {
private fun Content(leading: @Composable () -> Unit, text: String, textColor: Color, trailing: @Composable () -> Unit) {
ListItem(
modifier = Modifier.padding(horizontal = 8.dp),
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
headlineContent = { Text(text) },
headlineContent = { Text(text = text, color = textColor) },
leadingContent = { leading() },
trailingContent = { trailing() },
)

View file

@ -18,26 +18,16 @@
package com.geeksville.mesh.ui.settings.radio
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material.icons.twotone.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.CleaningServices
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -45,7 +35,6 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@ -63,64 +52,11 @@ import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.radio.components.WarningDialog
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
@Suppress("LongMethod")
@Composable
private fun NavButton(@StringRes title: Int, enabled: Boolean, onClick: () -> Unit) {
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
AlertDialog(
onDismissRequest = {},
shape = RoundedCornerShape(16.dp),
title = {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Icon(
imageVector = Icons.TwoTone.Warning,
contentDescription = stringResource(id = R.string.warning),
modifier = Modifier.padding(end = 8.dp),
)
Text(text = "${stringResource(title)}?\n")
Icon(
imageVector = Icons.TwoTone.Warning,
contentDescription = stringResource(id = R.string.warning),
modifier = Modifier.padding(start = 8.dp),
)
}
},
confirmButton = {
Row(
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
TextButton(modifier = Modifier.weight(1f), onClick = { showDialog = false }) {
Text(stringResource(R.string.cancel))
}
Button(
modifier = Modifier.weight(1f),
onClick = {
showDialog = false
onClick()
},
) {
Text(stringResource(R.string.send))
}
}
},
)
}
Column {
Spacer(modifier = Modifier.height(4.dp))
Button(modifier = Modifier.fillMaxWidth().height(48.dp), enabled = enabled, onClick = { showDialog = true }) {
Text(text = stringResource(title))
}
}
}
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun RadioConfigItemList(
state: RadioConfigState,
@ -189,8 +125,26 @@ fun RadioConfigItemList(
}
}
Column(modifier = Modifier.padding(top = 16.dp)) {
AdminRoute.entries.forEach { NavButton(it.title, enabled) { onRouteClick(it) } }
TitledCard(title = stringResource(R.string.administration), modifier = Modifier.padding(top = 16.dp)) {
AdminRoute.entries.forEach { route ->
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
WarningDialog(
title = "${stringResource(route.title)}?",
onDismiss = { showDialog = false },
onConfirm = { onRouteClick(route) },
)
}
SettingsItem(
enabled = enabled,
text = stringResource(route.title),
leadingIcon = route.icon,
trailingIcon = null,
) {
showDialog = true
}
}
}
TitledCard(title = stringResource(R.string.advanced_title), modifier = Modifier.padding(top = 16.dp)) {
@ -200,9 +154,17 @@ fun RadioConfigItemList(
SettingsItem(
text = stringResource(R.string.clean_node_database_title),
leadingIcon = Icons.Rounded.CleaningServices,
enabled = enabled,
onClick = { onNavigate(SettingsRoutes.CleanNodeDb) },
)
SettingsItem(
text = stringResource(R.string.debug_panel),
leadingIcon = Icons.Rounded.BugReport,
enabled = enabled,
onClick = { onNavigate(SettingsRoutes.DebugPanel) },
)
}
}

View file

@ -0,0 +1,63 @@
/*
* 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 com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.common.theme.AppTheme
@Composable
fun WarningDialog(
icon: ImageVector? = Icons.Rounded.Warning,
title: String,
onDismiss: () -> Unit,
onConfirm: () -> Unit,
) {
AlertDialog(
onDismissRequest = {},
icon = { icon?.let { Icon(imageVector = it, contentDescription = null) } },
title = { Text(text = title) },
dismissButton = { TextButton(onClick = { onDismiss() }) { Text(stringResource(R.string.cancel)) } },
confirmButton = {
Button(
onClick = {
onDismiss()
onConfirm()
},
) {
Text(stringResource(R.string.send))
}
},
)
}
@Preview
@Composable
private fun WarningDialogPreview() {
AppTheme { WarningDialog(title = "Factory Reset?", onDismiss = {}, onConfirm = {}) }
}

View file

@ -322,6 +322,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -316,6 +316,7 @@
<string name="udp_config">UDP конфигурација</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">Корисник</string>
<string name="channels">Канали</string>
<string name="device">Уређај</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">Конфигуриране на UDP</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Последно чут: %2$s<br>Последна позиция: %3$s<br>Батерия: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">Потребител</string>
<string name="channels">Канали</string>
<string name="device">Устройство</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -318,6 +318,7 @@
<string name="udp_config">UDP Konfigurace</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Naposledy slyšen: %2$s<br>Poslední pozice: %3$s<br>Baterie: %4$s]]></string>
<string name="toggle_my_position">Zapnout/vypnout pozici</string>
<string name="orient_north">Orient north</string>
<string name="user">Uživatel</string>
<string name="channels">Kanály</string>
<string name="device">Zařízení</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP Konfiguration</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Zuletzt gehört:%2$s<br>Letzte Position:%3$s<br>Akku:%4$s]]></string>
<string name="toggle_my_position">Position einschalten</string>
<string name="orient_north">Ausrichtung Nord</string>
<string name="user">Benutzer</string>
<string name="channels">Kanäle</string>
<string name="device">Gerät</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">Χρήστης</string>
<string name="channels">Κανάλια</string>
<string name="device">Συσκευή</string>

View file

@ -315,6 +315,7 @@ Rango de Valores 0 - 500.</string>
<string name="udp_config">Configuración UDP</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Última escucha: %2$s<br>Última posición: %3$s<br>Batería: %4$s]]></string>
<string name="toggle_my_position">Cambiar mi posición</string>
<string name="orient_north">Orient north</string>
<string name="user">Usuario</string>
<string name="channels">Canales</string>
<string name="device">Dispositivo</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP sätted</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Viimati kuuldud: %2$s<br>viimane asukoht: %3$s<br>Akupinge: %4$s]]></string>
<string name="toggle_my_position">Lülita asukoht sisse</string>
<string name="orient_north">Orient north</string>
<string name="user">Kasutaja</string>
<string name="channels">Kanal</string>
<string name="device">Seade</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP asetukset</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Viimeksi kuultu: %2$s<br>Viimeisin sijainti: %3$s<br>Akku: %4$s]]></string>
<string name="toggle_my_position">Kytke sijainti päälle</string>
<string name="orient_north">Aseta kompassi pohjoiseen</string>
<string name="user">Käyttäjä</string>
<string name="channels">Kanavat</string>
<string name="device">Laite</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">Configuration UDP</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Dernière écoute : %2$s<br>Dernière position : %3$s<br>Batterie : %4$s]]></string>
<string name="toggle_my_position">Basculer ma position</string>
<string name="orient_north">Orient north</string>
<string name="user">Utilisateur</string>
<string name="channels">Canaux</string>
<string name="device">Appareil</string>

View file

@ -320,6 +320,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -316,6 +316,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">Configurazione UDP</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Ricevuto l\'ultima volta: %2$s<br>Posizione più recente: %3$s<br>Batteria: %4$s]]></string>
<string name="toggle_my_position">Attiva/disattiva posizione</string>
<string name="orient_north">Orient north</string>
<string name="user">Utente</string>
<string name="channels">Canali</string>
<string name="device">Dispositivo</string>

View file

@ -318,6 +318,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -313,6 +313,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">自分の位置を切り替え</string>
<string name="orient_north">Orient north</string>
<string name="user">ユーザー</string>
<string name="channels">チャンネル</string>
<string name="device">接続するデバイスを選択</string>

View file

@ -312,6 +312,7 @@
<string name="udp_config">UDP 설정</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>최근 수신: %2$s<br>최근 위치: %3$s<br>배터리: %4$s]]></string>
<string name="toggle_my_position">내 위치 토글</string>
<string name="orient_north">Orient north</string>
<string name="user">사용자</string>
<string name="channels">채널</string>
<string name="device">장치</string>

View file

@ -318,6 +318,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP Configuratie</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Wissel mijn positie</string>
<string name="orient_north">Orient north</string>
<string name="user">Gebruiker</string>
<string name="channels">Kanalen</string>
<string name="device">Apparaat</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -318,6 +318,7 @@
<string name="udp_config">Ustawienia UDP</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Pokaż moją pozycję</string>
<string name="orient_north">Orient north</string>
<string name="user">Użytkownik</string>
<string name="channels">Kanały</string>
<string name="device">Urządzenie</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">Configuração UDP</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Última vez: %2$s<br>Última posição: %3$s<br>Bateria: %4$s]]></string>
<string name="toggle_my_position">Ativar minha posição</string>
<string name="orient_north">Orient north</string>
<string name="user">Usuário</string>
<string name="channels">Canais</string>
<string name="device">Dispositivo</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">Configuração UDP</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">Utilizador</string>
<string name="channels">Canal</string>
<string name="device">Dispositivo</string>

View file

@ -316,6 +316,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -318,6 +318,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Последний приём: %2$s<br>Последнее местоположение: %3$s<br>Батарея: %4$s]]></string>
<string name="toggle_my_position">Переключить мою позицию</string>
<string name="orient_north">Orient north</string>
<string name="user">Пользователь</string>
<string name="channels">Каналы</string>
<string name="device">Устройство</string>

View file

@ -318,6 +318,7 @@
<string name="udp_config">Konfigurácia UDP</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Zapnúť lokalizáciu</string>
<string name="orient_north">Orient north</string>
<string name="user">Užívateľ</string>
<string name="channels">Kanále</string>
<string name="device">Zariadenie</string>

View file

@ -318,6 +318,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP Config</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Channels</string>
<string name="device">Device</string>

View file

@ -316,6 +316,7 @@
<string name="udp_config">UDP конфигурација</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">Корисник</string>
<string name="channels">Канали</string>
<string name="device">Уређај</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP-konfiguration</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">User</string>
<string name="channels">Kanaler</string>
<string name="device">Device</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP Ayarları</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Konumunumu aç/kapa</string>
<string name="orient_north">Orient north</string>
<string name="user">Kullanıcı</string>
<string name="channels">Kanallar</string>
<string name="device">Cihaz</string>

View file

@ -318,6 +318,7 @@
<string name="udp_config">Налаштування UDP</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>Last heard: %2$s<br>Last position: %3$s<br>Battery: %4$s]]></string>
<string name="toggle_my_position">Toggle my position</string>
<string name="orient_north">Orient north</string>
<string name="user">Користувач</string>
<string name="channels">Канали</string>
<string name="device">Пристрій</string>

View file

@ -314,6 +314,7 @@
<string name="udp_config">UDP 设置</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>最后听到: %2$s<br>最后位置: %3$s<br>电量: %4$s]]></string>
<string name="toggle_my_position">切换我的位置</string>
<string name="orient_north">Orient north</string>
<string name="user">用户</string>
<string name="channels">频道</string>
<string name="device">设备</string>

View file

@ -312,6 +312,7 @@
<string name="udp_config">UDP設置</string>
<string name="map_node_popup_details"><![CDATA[%1$s<br>最後接收: %2$s<br>最後位置: %3$s<br>電量: %4$s]]></string>
<string name="toggle_my_position">切換我的位置</string>
<string name="orient_north">Orient north</string>
<string name="user">用戶</string>
<string name="channels">頻道</string>
<string name="device">裝置</string>

View file

@ -14,7 +14,7 @@ core-location-altitude = "1.0.0-alpha03"
core-splashscreen = "1.0.1"
crashlytics = "3.0.6"
datastore = "1.1.7"
dd-sdk-android = "2.26.0"
dd-sdk-android = "3.0.0"
dd-sdk-android-gradle-plugin = "1.19.0"
detekt = "1.23.8"
devtools-ksp = "2.2.10-2.0.2"
@ -94,6 +94,7 @@ dd-sdk-android-session-replay = { group = "com.datadoghq", name = "dd-sdk-androi
dd-sdk-android-session-replay-compose = { group = "com.datadoghq", name = "dd-sdk-android-session-replay-compose", version.ref = "dd-sdk-android" }
dd-sdk-android-timber = { group = "com.datadoghq", name = "dd-sdk-android-timber", version.ref = "dd-sdk-android" }
dd-sdk-android-trace = { group = "com.datadoghq", name = "dd-sdk-android-trace", version.ref = "dd-sdk-android" }
dd-sdk-android-trace-otel = { group = "com.datadoghq", name = "dd-sdk-android-trace-otel", version.ref = "dd-sdk-android" }
detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" }
emoji2-emojipicker = { group = "androidx.emoji2", name = "emoji2-emojipicker", version.ref = "emoji2" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
@ -193,7 +194,7 @@ maps-compose = ["location-services", "maps-compose", "maps-compose-utils", "maps
firebase = ["firebase-analytics", "firebase-crashlytics"]
# Datadog
datadog = ["dd-sdk-android-compose", "dd-sdk-android-logs", "dd-sdk-android-okhttp", "dd-sdk-android-rum", "dd-sdk-android-session-replay", "dd-sdk-android-session-replay-compose", "dd-sdk-android-timber", "dd-sdk-android-trace"]
datadog = ["dd-sdk-android-compose", "dd-sdk-android-logs", "dd-sdk-android-okhttp", "dd-sdk-android-rum", "dd-sdk-android-session-replay", "dd-sdk-android-session-replay-compose", "dd-sdk-android-timber", "dd-sdk-android-trace", "dd-sdk-android-trace-otel"]
# Protobuf
protobuf = ["protobuf-kotlin"]