diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index 06a14203f..191d7ba7a 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -1160,10 +1160,16 @@ Wirelessly manage your device settings and channels. Map style selection OpenStreetMap - Satellite + Light Terrain - Hybrid + 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 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 9b23f7003..561b05681 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 @@ -38,15 +38,22 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource import org.maplibre.compose.camera.CameraState import org.maplibre.compose.material3.OfflinePackListItem import org.maplibre.compose.offline.OfflinePackDefinition import org.maplibre.compose.offline.rememberOfflineManager +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.done +import org.meshtastic.core.resources.offline_download +import org.meshtastic.core.resources.offline_download_visible_region +import org.meshtastic.core.resources.offline_downloaded_regions +import org.meshtastic.core.resources.offline_maps +import org.meshtastic.core.resources.offline_saves_tiles +import org.meshtastic.core.resources.offline_unnamed_region import org.meshtastic.core.ui.icon.CloudDownload import org.meshtastic.core.ui.icon.MeshtasticIcons -@Composable actual fun isOfflineManagerAvailable(): Boolean = true - @Suppress("LongMethod") @Composable actual fun OfflineMapContent(styleUri: String, cameraState: CameraState) { @@ -55,9 +62,10 @@ actual fun OfflineMapContent(styleUri: String, cameraState: CameraState) { var showDialog by remember { mutableStateOf(false) } if (showDialog) { + val unnamedRegion = stringResource(Res.string.offline_unnamed_region) AlertDialog( onDismissRequest = { showDialog = false }, - title = { Text("Offline Maps") }, + title = { Text(stringResource(Res.string.offline_maps)) }, text = { Column(modifier = Modifier.fillMaxWidth()) { // Download button for current viewport @@ -85,13 +93,16 @@ actual fun OfflineMapContent(styleUri: String, cameraState: CameraState) { ) { Icon( imageVector = MeshtasticIcons.CloudDownload, - contentDescription = "Download", + contentDescription = stringResource(Res.string.offline_download), modifier = Modifier.padding(end = 16.dp), ) Column { - Text(text = "Download visible region", style = MaterialTheme.typography.bodyLarge) Text( - text = "Saves tiles for offline use", + text = stringResource(Res.string.offline_download_visible_region), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(Res.string.offline_saves_tiles), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -101,27 +112,27 @@ actual fun OfflineMapContent(styleUri: String, cameraState: CameraState) { // Existing packs if (offlineManager.packs.isNotEmpty()) { Text( - text = "Downloaded Regions", + text = stringResource(Res.string.offline_downloaded_regions), style = MaterialTheme.typography.titleSmall, modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), ) offlineManager.packs.toList().forEach { pack -> key(pack.hashCode()) { OfflinePackListItem(pack = pack, offlineManager = offlineManager) { - Text(pack.metadata?.decodeToString().orEmpty().ifBlank { "Unnamed Region" }) + Text(pack.metadata?.decodeToString().orEmpty().ifBlank { unnamedRegion }) } } } } } }, - confirmButton = { TextButton(onClick = { showDialog = false }) { Text("Done") } }, + confirmButton = { TextButton(onClick = { showDialog = false }) { Text(stringResource(Res.string.done)) } }, ) } // Expose the toggle via a side effect — the parent screen will call this // by rendering OfflineMapContent and using the showDialog state IconButton(onClick = { showDialog = true }) { - Icon(imageVector = MeshtasticIcons.CloudDownload, contentDescription = "Offline Maps") + Icon(imageVector = MeshtasticIcons.CloudDownload, contentDescription = stringResource(Res.string.offline_maps)) } } 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 f322b2ccb..4fa57f01d 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 @@ -199,6 +199,7 @@ fun MapScreen( LaunchedEffect(cameraState.moveReason) { if (cameraState.moveReason == CameraMoveReason.GESTURE && isLocationTrackingEnabled) { isLocationTrackingEnabled = false + bearingUpdate = BearingUpdate.IGNORE } } } diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt index ee130055f..734afcec1 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt @@ -18,14 +18,6 @@ package org.meshtastic.feature.map import androidx.compose.runtime.Composable -/** - * Returns `true` if the platform supports offline map tile management. - * - Android: `true` (backed by MapLibre Native). - * - iOS: `true` (backed by MapLibre Native). - * - Desktop/JS: `false` (no offline support). - */ -@Composable expect fun isOfflineManagerAvailable(): Boolean - /** * Renders platform-specific offline map management UI if the platform supports it. The composable receives the current * style URI and [cameraState] for downloading the visible region. diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt index 656e01f13..721dab9f2 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/MapControlsOverlay.kt @@ -34,7 +34,7 @@ import org.meshtastic.core.resources.map_filter import org.meshtastic.core.resources.orient_north import org.meshtastic.core.resources.refresh import org.meshtastic.core.resources.toggle_my_position -import org.meshtastic.core.ui.icon.LocationDisabled +import org.meshtastic.core.ui.icon.LocationOn import org.meshtastic.core.ui.icon.MapCompass import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.MyLocation @@ -116,13 +116,13 @@ fun MapControlsOverlay( } } - // Location tracking button — 3 states: Off (MyLocation), Tracking (LocationDisabled), TrackingBearing (NearMe) + // Location tracking button — 3 states: Off (MyLocation), Tracking (NearMe), TrackingNorth (LocationOn) MapButton( icon = when { !isLocationTrackingEnabled -> MeshtasticIcons.MyLocation isTrackingBearing -> MeshtasticIcons.NearMe - else -> MeshtasticIcons.LocationDisabled + else -> MeshtasticIcons.LocationOn }, contentDescription = stringResource(Res.string.toggle_my_position), iconTint = if (isLocationTrackingEnabled) MaterialTheme.colorScheme.primary else null, 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 a4c8fdd5d..8fdcf0e33 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 @@ -20,7 +20,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import org.maplibre.compose.expressions.dsl.asNumber +import kotlinx.serialization.json.jsonPrimitive +import org.maplibre.compose.expressions.dsl.asString import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.eq import org.maplibre.compose.expressions.dsl.feature @@ -81,7 +82,7 @@ fun NodeTrackLayers( strokeWidth = const(1.dp), strokeColor = const(Color.White), onClick = { features -> - val time = features.firstOrNull()?.properties?.get("time")?.toString()?.toIntOrNull() + val time = features.firstOrNull()?.properties?.get("time")?.jsonPrimitive?.content?.toIntOrNull() if (time != null && onPositionSelected != null) { onPositionSelected(time) ClickResult.Consume @@ -96,7 +97,7 @@ fun NodeTrackLayers( CircleLayer( id = "node-track-selected", source = pointsSource, - filter = feature["time"].asNumber() eq const(selectedPositionTime.toFloat()), + filter = feature["time"].asString() eq const(selectedPositionTime.toString()), radius = const(10.dp), color = const(SelectedPointColor), // Red strokeWidth = const(2.dp), diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/model/MapStyle.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/model/MapStyle.kt index 334521d68..339b69e50 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/model/MapStyle.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/model/MapStyle.kt @@ -20,9 +20,9 @@ import org.jetbrains.compose.resources.StringResource import org.maplibre.compose.style.BaseStyle import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.map_style_dark -import org.meshtastic.core.resources.map_style_hybrid +import org.meshtastic.core.resources.map_style_light import org.meshtastic.core.resources.map_style_osm -import org.meshtastic.core.resources.map_style_satellite +import org.meshtastic.core.resources.map_style_road_map import org.meshtastic.core.resources.map_style_terrain /** @@ -35,15 +35,15 @@ enum class MapStyle(val label: StringResource, val styleUri: String) { OpenStreetMap(label = Res.string.map_style_osm, styleUri = "https://tiles.openfreemap.org/styles/liberty"), /** Clean, light cartographic style via OpenFreeMap Positron. */ - Satellite(label = Res.string.map_style_satellite, styleUri = "https://tiles.openfreemap.org/styles/positron"), + Light(label = Res.string.map_style_light, styleUri = "https://tiles.openfreemap.org/styles/positron"), /** Topographic style via OpenFreeMap Bright. */ Terrain(label = Res.string.map_style_terrain, styleUri = "https://tiles.openfreemap.org/styles/bright"), /** US road-map style via Americana. */ - Hybrid(label = Res.string.map_style_hybrid, styleUri = "https://americanamap.org/style.json"), + RoadMap(label = Res.string.map_style_road_map, styleUri = "https://americanamap.org/style.json"), - /** Dark mode style via OpenFreeMap Bright (dark palette). */ + /** Dark mode style via OpenFreeMap Fiord. */ Dark(label = Res.string.map_style_dark, styleUri = "https://tiles.openfreemap.org/styles/fiord"), ; diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/util/GeoJsonConverters.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/util/GeoJsonConverters.kt index 622703f05..e111ad10a 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/util/GeoJsonConverters.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/util/GeoJsonConverters.kt @@ -123,7 +123,7 @@ fun positionsToPointFeatures(positions: List): Fe if (lat == 0.0 && lng == 0.0) return@mapNotNull null val props = buildJsonObject { - put("time", pos.time ?: 0) + put("time", (pos.time ?: 0).toString()) put("altitude", pos.altitude ?: 0) put("ground_speed", pos.ground_speed ?: 0) put("sats_in_view", pos.sats_in_view ?: 0) diff --git a/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt b/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt index 819cf1708..1f4acf1c4 100644 --- a/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt +++ b/feature/map/src/iosMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt @@ -38,15 +38,22 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource import org.maplibre.compose.camera.CameraState import org.maplibre.compose.material3.OfflinePackListItem import org.maplibre.compose.offline.OfflinePackDefinition import org.maplibre.compose.offline.rememberOfflineManager +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.done +import org.meshtastic.core.resources.offline_download +import org.meshtastic.core.resources.offline_download_visible_region +import org.meshtastic.core.resources.offline_downloaded_regions +import org.meshtastic.core.resources.offline_maps +import org.meshtastic.core.resources.offline_saves_tiles +import org.meshtastic.core.resources.offline_unnamed_region import org.meshtastic.core.ui.icon.CloudDownload import org.meshtastic.core.ui.icon.MeshtasticIcons -@Composable actual fun isOfflineManagerAvailable(): Boolean = true - @Suppress("LongMethod") @Composable actual fun OfflineMapContent(styleUri: String, cameraState: CameraState) { @@ -55,9 +62,10 @@ actual fun OfflineMapContent(styleUri: String, cameraState: CameraState) { var showDialog by remember { mutableStateOf(false) } if (showDialog) { + val unnamedRegion = stringResource(Res.string.offline_unnamed_region) AlertDialog( onDismissRequest = { showDialog = false }, - title = { Text("Offline Maps") }, + title = { Text(stringResource(Res.string.offline_maps)) }, text = { Column(modifier = Modifier.fillMaxWidth()) { Row( @@ -84,13 +92,16 @@ actual fun OfflineMapContent(styleUri: String, cameraState: CameraState) { ) { Icon( imageVector = MeshtasticIcons.CloudDownload, - contentDescription = "Download", + contentDescription = stringResource(Res.string.offline_download), modifier = Modifier.padding(end = 16.dp), ) Column { - Text(text = "Download visible region", style = MaterialTheme.typography.bodyLarge) Text( - text = "Saves tiles for offline use", + text = stringResource(Res.string.offline_download_visible_region), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringResource(Res.string.offline_saves_tiles), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -99,25 +110,25 @@ actual fun OfflineMapContent(styleUri: String, cameraState: CameraState) { if (offlineManager.packs.isNotEmpty()) { Text( - text = "Downloaded Regions", + text = stringResource(Res.string.offline_downloaded_regions), style = MaterialTheme.typography.titleSmall, modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), ) offlineManager.packs.toList().forEach { pack -> key(pack.hashCode()) { OfflinePackListItem(pack = pack, offlineManager = offlineManager) { - Text(pack.metadata?.decodeToString().orEmpty().ifBlank { "Unnamed Region" }) + Text(pack.metadata?.decodeToString().orEmpty().ifBlank { unnamedRegion }) } } } } } }, - confirmButton = { TextButton(onClick = { showDialog = false }) { Text("Done") } }, + confirmButton = { TextButton(onClick = { showDialog = false }) { Text(stringResource(Res.string.done)) } }, ) } IconButton(onClick = { showDialog = true }) { - Icon(imageVector = MeshtasticIcons.CloudDownload, contentDescription = "Offline Maps") + Icon(imageVector = MeshtasticIcons.CloudDownload, contentDescription = stringResource(Res.string.offline_maps)) } } diff --git a/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt b/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt index bda1f9aa1..e80a5eed6 100644 --- a/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt +++ b/feature/map/src/jvmMain/kotlin/org/meshtastic/feature/map/OfflineManagerFactory.kt @@ -19,8 +19,6 @@ package org.meshtastic.feature.map import androidx.compose.runtime.Composable import org.maplibre.compose.camera.CameraState -@Composable actual fun isOfflineManagerAvailable(): Boolean = false - @Composable actual fun OfflineMapContent(styleUri: String, cameraState: CameraState) { // Offline map management is not available on Desktop.