diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt
index f6884c99d..5a92c0e05 100644
--- a/.skills/compose-ui/strings-index.txt
+++ b/.skills/compose-ui/strings-index.txt
@@ -698,7 +698,12 @@ map_reporting_interval_seconds
map_reporting_summary
map_select_download_region
map_start_download
+map_style_dark
+map_style_light
+map_style_osm
+map_style_road_map
map_style_selection
+map_style_terrain
map_subDescription
map_tile_download_estimate
map_tile_source
@@ -887,6 +892,13 @@ notifications_on_message_receipt
now
ntp_server
number_of_records
+### OFFLINE ###
+offline_download
+offline_download_visible_region
+offline_downloaded_regions
+offline_maps
+offline_saves_tiles
+offline_unnamed_region
ok_to_mqtt
okay
oled_type
@@ -1419,3 +1431,4 @@ wind_speed
you
zh_CN
zh_TW
+zoom_to_fit_all
diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts
index 81896d010..e68e5c6cb 100644
--- a/androidApp/build.gradle.kts
+++ b/androidApp/build.gradle.kts
@@ -263,7 +263,6 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.glance.preview)
- googleImplementation(libs.dd.sdk.android.compose)
googleImplementation(libs.dd.sdk.android.logs)
googleImplementation(libs.dd.sdk.android.rum)
googleImplementation(libs.dd.sdk.android.session.replay)
diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml
index aa8c1fdac..9ea2aaf17 100644
--- a/core/resources/src/commonMain/composeResources/values/strings.xml
+++ b/core/resources/src/commonMain/composeResources/values/strings.xml
@@ -728,7 +728,12 @@
Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name.
Select download region
Start Download
+ Dark
+ Light
+ OpenStreetMap
+ Road Map
Map style selection
+ Terrain
bearing: %1$d° distance: %2$s
Tile download estimate:
Tile Source
@@ -917,6 +922,13 @@
Now
NTP server
Number of records
+
+ Download
+ Download visible region
+ Downloaded Regions
+ Offline Maps
+ Saves tiles for offline use
+ Unnamed Region
Ok to MQTT
OK
OLED type
@@ -1248,53 +1260,6 @@
RX Boosted Gain
System Settings
- Generate QR Code
- NFC is disabled. Please enable it in system settings.
- All
-
- Bluetooth
- Configure Bluetooth Permissions
- Discovery
- Find and identify Meshtastic devices near you.
- Configuration
- Wirelessly manage your device settings and channels.
- Map style selection
- OpenStreetMap
- Light
- Terrain
- Road Map
- Dark
- Offline Maps
- Download
- Download visible region
- Saves tiles for offline use
- Downloaded Regions
- Unnamed Region
-
- Battery: %1$d%
- Nodes: %1$d online / %2$d total
- Uptime: %1$s
- ChUtil: %1$s% | AirTX: %2$s%
- Traffic: TX %1$d / RX %2$d (D: %3$d)
- Relays: %1$d (Canceled: %2$d)
- Diagnostics: %1$s
- Noise %1$d dBm
- Bad %1$d
- Dropped %1$d
- Heap
- %1$d / %2$d
- %1$s
- Powered
- Refresh
- Updated
-
-
- Add Network Layer
- https://example.com/map.kml or .geojson
-
- Local MBTiles File
- Add Local MBTiles File
-
TAK (ATAK)
TAK Configuration
Member Role
@@ -1457,6 +1422,7 @@
Warning
Delete waypoint?
Edit waypoint
+ Lock to my node
New waypoint
Received waypoint: %1$s
Weight
diff --git a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt
index 561b05681..26856b139 100644
--- a/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt
+++ b/feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt
@@ -54,7 +54,7 @@ import org.meshtastic.core.resources.offline_unnamed_region
import org.meshtastic.core.ui.icon.CloudDownload
import org.meshtastic.core.ui.icon.MeshtasticIcons
-@Suppress("LongMethod")
+@Suppress("LongMethod", "ModifierMissing")
@Composable
actual fun OfflineMapContent(styleUri: String, cameraState: CameraState) {
val offlineManager = rememberOfflineManager()
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt
index 594272bf9..8027020a1 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/MapScreen.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025-2026 Meshtastic LLC
+ * Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -72,8 +72,8 @@ private val MAP_OVERLAY_PADDING = 16.dp
fun MapScreen(
onClickNodeChip: (Int) -> Unit,
navigateToNodeDetails: (Int) -> Unit,
- modifier: Modifier = Modifier,
viewModel: MapViewModel,
+ modifier: Modifier = Modifier,
waypointId: Int? = null,
) {
val ourNodeInfo by viewModel.ourNodeInfo.collectAsStateWithLifecycle()
@@ -163,7 +163,7 @@ fun MapScreen(
modifier = Modifier.fillMaxSize(),
gestureOptions = gestureOptions,
styleState = styleState,
- onCameraMoved = { position -> viewModel.saveCameraPosition(position) },
+ onCameraMove = { position -> viewModel.saveCameraPosition(position) },
onWaypointClick = { wpId ->
editingWaypointId = wpId
longPressPosition = null
@@ -235,10 +235,12 @@ fun MapScreen(
// TrackBearing → TrackNorth
bearingUpdate = BearingUpdate.ALWAYS_NORTH
}
+
BearingUpdate.ALWAYS_NORTH -> {
// TrackNorth → Off
isLocationTrackingEnabled = false
}
+
BearingUpdate.IGNORE -> {
isLocationTrackingEnabled = false
}
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
index 5a2b408e9..8578885c4 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/EditWaypointDialog.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025-2026 Meshtastic LLC
+ * Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterDropdown.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterDropdown.kt
index 5b8cb5993..015fd4756 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterDropdown.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapFilterDropdown.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025-2026 Meshtastic LLC
+ * Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapStyleSelector.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapStyleSelector.kt
index a50c9e2a7..9d3993aab 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapStyleSelector.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapStyleSelector.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025-2026 Meshtastic LLC
+ * Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt
index 63ad11aa7..6a5b022ee 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MaplibreMapContent.kt
@@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@@ -121,10 +122,10 @@ fun MaplibreMapContent(
modifier: Modifier = Modifier,
gestureOptions: GestureOptions = GestureOptions.Standard,
styleState: StyleState = rememberStyleState(),
- onCameraMoved: (CameraPosition) -> Unit = {},
+ onCameraMove: (CameraPosition) -> Unit = {},
onWaypointClick: (Int) -> Unit = {},
- onMapLoadFinished: () -> Unit = {},
- onMapLoadFailed: (String?) -> Unit = {},
+ onMapLoad: () -> Unit = {},
+ onMapLoadFail: (String?) -> Unit = {},
locationState: UserLocationState? = null,
) {
MaplibreMap(
@@ -137,8 +138,8 @@ fun MaplibreMapContent(
onMapLongClick(position)
ClickResult.Consume
},
- onMapLoadFinished = onMapLoadFinished,
- onMapLoadFailed = onMapLoadFailed,
+ onMapLoadFinished = onMapLoad,
+ onMapLoadFailed = onMapLoadFail,
) {
// --- Terrain hillshade overlay ---
if (showHillshade) {
@@ -172,9 +173,10 @@ fun MaplibreMapContent(
}
// Persist camera position when it stops moving
+ val currentOnCameraMove = rememberUpdatedState(onCameraMove)
LaunchedEffect(cameraState.isCameraMoving) {
if (!cameraState.isCameraMoving) {
- onCameraMoved(cameraState.position)
+ currentOnCameraMove.value(cameraState.position)
}
}
}
@@ -216,7 +218,7 @@ private fun NodeMarkerLayers(
cameraState.animateTo(
cameraState.position.copy(
target = target,
- zoom = cameraState.position.zoom + CLUSTER_ZOOM_INCREMENT,
+ zoom = minOf(cameraState.position.zoom + CLUSTER_ZOOM_INCREMENT, PRECISION_ZOOM_MAX.toDouble()),
),
)
}
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackLayers.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackLayers.kt
index fe38673c3..b07d92a5b 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackLayers.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackLayers.kt
@@ -49,7 +49,7 @@ private const val SELECTED_OPACITY = 0.9f
internal fun NodeTrackLayers(
positions: List,
selectedPositionTime: Int? = null,
- onPositionSelected: ((Int) -> Unit)? = null,
+ onSelectPosition: ((Int) -> Unit)? = null,
) {
if (positions.size < 2) return
@@ -87,8 +87,8 @@ internal fun NodeTrackLayers(
strokeColor = const(Color.White),
onClick = { features ->
val time = features.firstOrNull()?.properties?.get("time")?.jsonPrimitive?.content?.toIntOrNull()
- if (time != null && onPositionSelected != null) {
- onPositionSelected(time)
+ if (time != null && onSelectPosition != null) {
+ onSelectPosition(time)
ClickResult.Consume
} else {
ClickResult.Pass
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt
index 78091b1fc..619245a9f 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/NodeTrackMap.kt
@@ -39,8 +39,8 @@ private const val BOUNDS_PADDING_DP = 48
/**
* Embeddable position-track map showing a polyline with markers for the given positions.
*
- * Supports synchronized selection: [selectedPositionTime] highlights the corresponding marker and [onPositionSelected]
- * is called when a marker is tapped, passing the `Position.time` for the host screen to synchronize its card list.
+ * Supports synchronized selection: [selectedPositionTime] highlights the corresponding marker and [onSelectPosition] is
+ * called when a marker is tapped, passing the `Position.time` for the host screen to synchronize its card list.
*
* Replaces both the Google Maps and OSMDroid flavor-specific NodeTrackMap implementations.
*/
@@ -49,7 +49,7 @@ fun NodeTrackMap(
positions: List,
modifier: Modifier = Modifier,
selectedPositionTime: Int? = null,
- onPositionSelected: ((Int) -> Unit)? = null,
+ onSelectPosition: ((Int) -> Unit)? = null,
) {
val geoPositions =
remember(positions) { positions.mapNotNull { pos -> toGeoPositionOrNull(pos.latitude_i, pos.longitude_i) } }
@@ -82,7 +82,7 @@ fun NodeTrackMap(
NodeTrackLayers(
positions = positions,
selectedPositionTime = selectedPositionTime,
- onPositionSelected = onPositionSelected,
+ onSelectPosition = onSelectPosition,
)
}
}
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteLayers.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteLayers.kt
index 9aa2826dc..22e88f8b1 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteLayers.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteLayers.kt
@@ -19,6 +19,7 @@ package org.meshtastic.feature.map.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
@@ -66,7 +67,7 @@ internal fun TracerouteLayers(
overlay: TracerouteOverlay?,
nodePositions: Map,
nodes: Map,
- onMappableCountChanged: (shown: Int, total: Int) -> Unit,
+ onMappableCountChange: (shown: Int, total: Int) -> Unit,
) {
if (overlay == null) return
@@ -81,7 +82,8 @@ internal fun TracerouteLayers(
// Report mappable count via side effect (avoid state updates during composition)
val mappableCount = routeData.hopFeatures.features.size
val totalCount = overlay.relatedNodeNums.size
- LaunchedEffect(mappableCount, totalCount) { onMappableCountChanged(mappableCount, totalCount) }
+ val currentOnMappableCountChange = rememberUpdatedState(onMappableCountChange)
+ LaunchedEffect(mappableCount, totalCount) { currentOnMappableCountChange.value(mappableCount, totalCount) }
// Forward route line
if (routeData.forwardLine.features.isNotEmpty()) {
diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt
index 68a93726d..f1523e757 100644
--- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt
+++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/TracerouteMap.kt
@@ -53,7 +53,7 @@ private const val BOUNDS_PADDING_DP = 64
fun TracerouteMap(
tracerouteOverlay: TracerouteOverlay?,
tracerouteNodePositions: Map,
- onMappableCountChanged: (shown: Int, total: Int) -> Unit,
+ onMappableCountChange: (shown: Int, total: Int) -> Unit,
modifier: Modifier = Modifier,
nodes: Map = emptyMap(),
) {
@@ -91,7 +91,7 @@ fun TracerouteMap(
overlay = tracerouteOverlay,
nodePositions = tracerouteNodePositions,
nodes = nodes,
- onMappableCountChanged = onMappableCountChanged,
+ onMappableCountChange = onMappableCountChange,
)
}
}
diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt
index 14061cc76..25ed83620 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.kt
@@ -67,7 +67,7 @@ fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
positions = positions,
modifier = modifier,
selectedPositionTime = selectedTime,
- onPositionSelected = { time -> onPointSelected(time.toDouble()) },
+ onSelectPosition = { time -> onPointSelected(time.toDouble()) },
)
},
listPart = { modifier, selectedX, lazyListState, onCardClick ->
diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt
index 83079acb3..2b49b4bdc 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.kt
@@ -118,7 +118,7 @@ private fun TracerouteMapScaffold(
TracerouteMap(
tracerouteOverlay = overlay,
tracerouteNodePositions = snapshotPositions,
- onMappableCountChanged = { shown: Int, total: Int ->
+ onMappableCountChange = { shown: Int, total: Int ->
tracerouteNodesShown = shown
tracerouteNodesTotal = total
},