Kmp strings cleanup (#3669)

This commit is contained in:
Phil Oliver
2025-11-11 18:40:09 -05:00
committed by GitHub
parent bde7c47931
commit 57ef889caa
6 changed files with 341 additions and 209 deletions

View File

@@ -176,7 +176,7 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
private fun createNotificationChannel(type: NotificationType) {
if (notificationManager.getNotificationChannel(type.channelId) != null) return
val channelName = context.getString(type.channelNameRes)
val channelName = getString(type.channelNameRes)
val channel =
NotificationChannel(type.channelId, channelName, type.importance).apply {
lightColor = NOTIFICATION_LIGHT_COLOR
@@ -259,7 +259,7 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
null
}
cachedMessage = message ?: cachedMessage ?: context.getString(Res.string.no_local_stats)
cachedMessage = message ?: cachedMessage ?: getString(Res.string.no_local_stats)
val notification =
createServiceStateNotification(
@@ -295,7 +295,7 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
override fun showClientNotification(clientNotification: MeshProtos.ClientNotification) {
val notification =
createClientNotification(context.getString(Res.string.client_notification), clientNotification.message)
createClientNotification(getString(Res.string.client_notification), clientNotification.message)
notificationManager.notify(clientNotification.toString().hashCode(), notification)
}
@@ -375,7 +375,7 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
}
private fun createNewNodeSeenNotification(name: String, message: String?): Notification {
val title = context.getString(Res.string.new_node_seen).format(name)
val title = getString(Res.string.new_node_seen).format(name)
val builder =
commonBuilder(NotificationType.NewNode)
.setCategory(Notification.CATEGORY_STATUS)
@@ -393,9 +393,8 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
private fun createLowBatteryNotification(node: NodeEntity, isRemote: Boolean): Notification {
val type = if (isRemote) NotificationType.LowBatteryRemote else NotificationType.LowBatteryLocal
val title = context.getString(Res.string.low_battery_title).format(node.shortName)
val message =
context.getString(Res.string.low_battery_message).format(node.longName, node.deviceMetrics.batteryLevel)
val title = getString(Res.string.low_battery_title).format(node.shortName)
val message = getString(Res.string.low_battery_message).format(node.longName, node.deviceMetrics.batteryLevel)
return commonBuilder(type)
.setCategory(Notification.CATEGORY_STATUS)
@@ -445,7 +444,7 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva
}
private fun createReplyAction(contactKey: String): NotificationCompat.Action {
val replyLabel = context.getString(Res.string.reply)
val replyLabel = getString(Res.string.reply)
val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(replyLabel).build()
val replyIntent =

View File

@@ -17,23 +17,13 @@
package com.meshtastic.core.strings
import android.content.Context
import android.content.res.Resources
import kotlinx.coroutines.runBlocking
import org.jetbrains.compose.resources.StringResource
fun Context.getString(stringResource: StringResource): String = runBlocking {
fun getString(stringResource: StringResource): String = runBlocking {
org.jetbrains.compose.resources.getString(stringResource)
}
fun Context.getString(stringResource: StringResource, vararg formatArgs: Any): String = runBlocking {
org.jetbrains.compose.resources.getString(stringResource, *formatArgs)
}
fun Resources.getString(stringResource: StringResource): String = runBlocking {
org.jetbrains.compose.resources.getString(stringResource)
}
fun Resources.getString(stringResource: StringResource, vararg formatArgs: Any): String = runBlocking {
fun getString(stringResource: StringResource, vararg formatArgs: Any): String = runBlocking {
org.jetbrains.compose.resources.getString(stringResource, *formatArgs)
}

View File

@@ -23,7 +23,7 @@
<string name="zh_TW" translatable="false">繁體中文</string>
<string name="some_username" translatable="false">SKH</string>
<string name="sample_message" translatable="false">hey I found the cache, it is over here next to the big tiger. I\'m kinda scared.</string>
<string name="sample_message" translatable="false">hey I found the cache, it is over here next to the big tiger. I'm kinda scared.</string>
<string name="default_mqtt_address" translatable="false">mqtt.meshtastic.org</string>
@@ -99,7 +99,7 @@
<string name="rebroadcast_mode_local_only">Local Only</string>
<string name="rebroadcast_mode_local_only_desc">Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels.</string>
<string name="rebroadcast_mode_known_only">Known Only</string>
<string name="rebroadcast_mode_known_only_desc">Ignores observed messages from foreign meshes like LOCAL ONLY, but takes it step further by also ignoring messages from nodes not already in the node\'s known list.</string>
<string name="rebroadcast_mode_known_only_desc">Ignores observed messages from foreign meshes like LOCAL ONLY, but takes it step further by also ignoring messages from nodes not already in the node's known list.</string>
<string name="rebroadcast_mode_none">None</string>
<string name="rebroadcast_mode_none_desc">Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role.</string>
<string name="rebroadcast_mode_core_portnums_only">Core Portnums Only</string>
@@ -147,7 +147,7 @@
<string name="config_position_gps_update_interval_summary">How often should we try to get a GPS position (&lt;10sec keeps GPS on).</string>
<string name="config_position_flags_summary">Optional fields to include when assembling position messages. the more fields are included, the larger the message will be - leading to longer airtime and a higher risk of packet loss.</string>
<string name="config_power_is_power_saving_summary">Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don\'t use this setting if you want to use your device with the phone apps or are using a device without a user button.</string>
<string name="config_power_is_power_saving_summary">Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button.</string>
<string name="config_security_public_key">Generated from your public key and sent out to other nodes on the mesh to allow them to compute a shared secret key.</string>
<string name="config_security_private_key">Used to create a shared key with a remote device.</string>
@@ -181,7 +181,7 @@
<string name="application_icon">application icon</string>
<string name="unknown_username">Unknown Username</string>
<string name="send">Send</string>
<string name="warning_not_paired">You haven\'t yet paired a Meshtastic compatible radio with this phone. Please pair a device and set your username.\n\nThis open-source application is in development, if you find problems please post on our forum: https://github.com/orgs/meshtastic/discussions.\n\nFor more information see our web page - www.meshtastic.org.</string>
<string name="warning_not_paired">You haven't yet paired a Meshtastic compatible radio with this phone. Please pair a device and set your username.\n\nThis open-source application is in development, if you find problems please post on our forum: https://github.com/orgs/meshtastic/discussions.\n\nFor more information see our web page - www.meshtastic.org.</string>
<string name="you">You</string>
<string name="analytics_okay">Allow analytics and crash reporting.</string>
<string name="accept">Accept</string>
@@ -198,14 +198,14 @@
<string name="pairing_failed_try_again">Pairing failed, please select again</string>
<string name="location_disabled">Location access is turned off, can not provide position to mesh.</string>
<string name="share">Share</string>
<string name="new_node_seen">New Node Seen: %s</string>
<string name="new_node_seen">New Node Seen: %1$s</string>
<string name="disconnected">Disconnected</string>
<string name="device_sleeping">Device sleeping</string>
<string name="connected_count">Connected: %1$s online</string>
<string name="ip_address">IP Address:</string>
<string name="ip_port">Port:</string>
<string name="connected">Connected</string>
<string name="connected_to">Connected to radio (%s)</string>
<string name="connected_to">Connected to radio (%1$s)</string>
<string name="connection_status">Current connections:</string>
<string name="wifi_ip">Wifi IP:</string>
<string name="ethernet_ip">Ethernet IP:</string>
@@ -246,7 +246,7 @@
<string name="firmware_old">The radio firmware is too old to talk to this application. For more information on this see <a href="https://meshtastic.org/docs/getting-started/flashing-firmware">our Firmware Installation guide</a>.</string>
<string name="okay">OK</string>
<string name="must_set_region">You must set a region!</string>
<string name="cant_change_no_radio">Couldn\'t change channel, because radio is not yet connected. Please try again.</string>
<string name="cant_change_no_radio">Couldn't change channel, because radio is not yet connected. Please try again.</string>
<string name="save_rangetest">Export rangetest packets</string>
<string name="export_data_csv">Export all packets</string>
<string name="reset">Reset</string>
@@ -263,7 +263,7 @@
<string name="provide_location_to_mesh">Provide phone location to mesh</string>
<plurals name="delete_messages">
<item quantity="one">Delete message?</item>
<item quantity="other">Delete %s messages?</item>
<item quantity="other">Delete %1$s messages?</item>
</plurals>
<string name="delete">Delete</string>
<string name="delete_for_everyone">Delete for everyone</string>
@@ -297,15 +297,15 @@
<string name="bluetooth_disabled">Bluetooth is disabled. Please enable it in your device settings.</string>
<string name="open_settings">Open settings</string>
<string name="firmware_version">Firmware version: %1$s</string>
<string name="permission_missing_31">Meshtastic needs \"Nearby devices\" permissions enabled to find and connect to devices via Bluetooth. You can disable when not in use.</string>
<string name="permission_missing_31">Meshtastic needs "Nearby devices" permissions enabled to find and connect to devices via Bluetooth. You can disable when not in use.</string>
<string name="direct_message">Direct Message</string>
<string name="nodedb_reset">NodeDB reset</string>
<string name="delivery_confirmed">Delivery confirmed</string>
<string name="error">Error</string>
<string name="ignore">Ignore</string>
<string name="remove_ignored">Remove from ignored</string>
<string name="ignore_add">Add \'%s\' to ignore list?</string>
<string name="ignore_remove">Remove \'%s\' from ignore list?</string>
<string name="ignore_add">Add '%1$s' to ignore list?</string>
<string name="ignore_remove">Remove '%1$s' from ignore list?</string>
<string name="map_select_download_region">Select download region</string>
<string name="map_tile_download_estimate">Tile download estimate:</string>
<string name="map_start_download">Start Download</string>
@@ -318,10 +318,10 @@
<string name="calculating">Calculating…</string>
<string name="map_offline_manager">Offline Manager</string>
<string name="map_cache_size">Current Cache size</string>
<string name="map_cache_info">Cache Capacity: %1$.2f MB\nCache Usage: %2$.2f MB</string>
<string name="map_cache_info">Cache Capacity: %1$d MB\nCache Usage: %2$d MB</string>
<string name="map_clear_tiles">Clear Downloaded Tiles</string>
<string name="map_tile_source">Tile Source</string>
<string name="map_purge_success">SQL Cache purged for %s</string>
<string name="map_purge_success">SQL Cache purged for %1$s</string>
<string name="map_purge_fail">SQL Cache purge failed, see logcat for details</string>
<string name="map_cache_manager">Cache Manager</string>
<string name="map_download_complete">Download complete!</string>
@@ -331,7 +331,7 @@
<string name="waypoint_edit">Edit waypoint</string>
<string name="waypoint_delete">Delete waypoint?</string>
<string name="waypoint_new">New waypoint</string>
<string name="waypoint_received">Received waypoint: %s</string>
<string name="waypoint_received">Received waypoint: %1$s</string>
<string name="error_duty_cycle">Duty Cycle limit reached. Cannot send messages right now, please try again later.</string>
<string name="remove">Remove</string>
<string name="remove_node_text">This node will be removed from your list until your node receives data from it again.</string>
@@ -412,8 +412,8 @@
<string name="favorite">Favorite</string>
<string name="add_favorite">Add to favorites</string>
<string name="remove_favorite">Remove from favorites</string>
<string name="favorite_add">Add \'%s\' as a favorite node?</string>
<string name="favorite_remove">Remove \'%s\' as a favorite node?</string>
<string name="favorite_add">Add '%1$s' as a favorite node?</string>
<string name="favorite_remove">Remove '%1$s' as a favorite node?</string>
<string name="power_metrics_log">Power Metrics Log</string>
<string name="channel_1">Channel 1</string>
<string name="channel_2">Channel 2</string>
@@ -422,10 +422,10 @@
<string name="voltage">Voltage</string>
<string name="are_you_sure">Are you sure?</string>
<string name="router_role_confirmation_text"><![CDATA[I have read the <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">Device Role Documentation</a> and the blog post about <a href="http://meshtastic.org/blog/choosing-the-right-device-role">Choosing The Right Device Role</a>.]]></string>
<string name="i_know_what_i_m_doing">I know what I\'m doing.</string>
<string name="i_know_what_i_m_doing">I know what I'm doing.</string>
<string name="low_battery_message">Node %1$s has a low battery (%2$d%%)</string>
<string name="meshtastic_low_battery_notifications">Low battery notifications</string>
<string name="low_battery_title">Low battery: %s</string>
<string name="low_battery_title">Low battery: %1$s</string>
<string name="meshtastic_low_battery_temporary_remote_notifications">Low battery notifications (favorite nodes)</string>
<string name="baro_pressure">Barometric Pressure</string>
<string name="udp_enabled">Enabled</string>
@@ -866,7 +866,7 @@
<string name="critical_alerts_description">Select packets sent as critical will ignore the mute switch and Do Not Disturb settings in the OS notification center.</string>
<string name="configure_notification_permissions">Configure notification permissions</string>
<string name="phone_location">Phone Location</string>
<string name="phone_location_description">Meshtastic uses your phone\'s location to enable a number of features. You can update your location permissions at any time from settings.</string>
<string name="phone_location_description">Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from settings.</string>
<string name="share_location">Share Location</string>
<string name="share_location_description">Use your phone GPS to send locations to your node to instead of using a hardware GPS on your node.</string>
<string name="distance_measurements">Distance Measurements</string>
@@ -922,7 +922,7 @@
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node\'s configured gateway.</string>
<string name="uplink_feature_description">Messages from the mesh will be sent to the public internet through any node's configured gateway.</string>
<string name="downlink_feature_description">Messages from a public internet gateway are forwarded to the local mesh. Due to the zero-hop policy, traffic from the default MQTT server will not propagate further than this device.</string>
<string name="icon_meanings">Icon Meanings</string>
<string name="secondary_channel_position_feature">Disabling position on the primary channel allows periodic position broadcasts on the first secondary channel with the position enabled, otherwise manual position request required.</string>
@@ -935,7 +935,7 @@
<string name="eight_hours">8 Hours</string>
<string name="one_day">24 Hours</string>
<string name="two_days">48 Hours</string>
<string name="last_heard_filter_label">Filter by Last Heard time: %s</string>
<string name="last_heard_filter_label">Filter by Last Heard time: %1$s</string>
<string name="dbm_value">%1$d dBm</string>
<string name="error_no_app_to_handle_link">No application available to handle link.</string>
<string name="system_settings">System Settings</string>
@@ -949,6 +949,6 @@
<string name="for_more_information_see_our_privacy_policy">For more information, see our privacy policy.</string>
<string name="privacy_url" translatable="false">" https://meshtastic.org/docs/legal/privacy/"</string>
<string name="unset">Unset - 0</string>
<string name="relayed_by">Relayed by: %s</string>
<string name="relayed_by">Relayed by: %1$s</string>
<string name="preserve_favorites">Preserve Favorites?</string>
</resources>

View File

@@ -18,16 +18,20 @@
package org.meshtastic.feature.map
import android.Manifest // Added for Accompanist
import android.content.Context
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lens
import androidx.compose.material.icons.filled.LocationDisabled
@@ -36,17 +40,26 @@ import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.Layers
import androidx.compose.material.icons.outlined.MyLocation
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -57,7 +70,6 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -65,8 +77,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi // Added for Accompanist
import com.google.accompanist.permissions.rememberMultiplePermissionsState // Added for Accompanist
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.meshtastic.core.strings.getString
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.gpsDisabled
@@ -106,6 +118,8 @@ import org.meshtastic.core.strings.show_waypoints
import org.meshtastic.core.strings.toggle_my_position
import org.meshtastic.core.strings.waypoint_delete
import org.meshtastic.core.strings.you
import org.meshtastic.core.ui.component.BasicListItem
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.feature.map.cluster.RadiusMarkerClusterer
import org.meshtastic.feature.map.component.CacheLayout
@@ -194,41 +208,6 @@ private fun cacheManagerCallback(onTaskComplete: () -> Unit, onTaskFailed: (Int)
}
}
private fun Context.purgeTileSource(onResult: (String) -> Unit) {
val cache = SqlTileWriterExt()
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(getString(Res.string.map_tile_source))
val sources = cache.sources
val sourceList = mutableListOf<String>()
for (i in sources.indices) {
sourceList.add(sources[i].source as String)
}
val selected: BooleanArray? = null
val selectedList = mutableListOf<Int>()
builder.setMultiChoiceItems(sourceList.toTypedArray(), selected) { _, i, b ->
if (b) {
selectedList.add(i)
} else {
selectedList.remove(i)
}
}
builder.setPositiveButton(getString(Res.string.clear)) { _, _ ->
for (x in selectedList) {
val item = sources[x]
val b = cache.purgeCache(item.source)
onResult(
if (b) {
getString(Res.string.map_purge_success, item.source.toString())
} else {
getString(Res.string.map_purge_fail)
},
)
}
}
builder.setNegativeButton(getString(Res.string.cancel)) { dialog, _ -> dialog.cancel() }
builder.show()
}
/**
* Main composable for displaying the map view, including nodes, waypoints, and user location. It handles user
* interactions for map manipulation, filtering, and offline caching.
@@ -255,11 +234,13 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
var showDownloadButton: Boolean by remember { mutableStateOf(false) }
var showEditWaypointDialog by remember { mutableStateOf<Waypoint?>(null) }
var showCacheManagerDialog by remember { mutableStateOf(false) }
var showCurrentCacheInfo by remember { mutableStateOf(false) }
var showPurgeTileSourceDialog by remember { mutableStateOf(false) }
var showMapStyleDialog by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val context = LocalContext.current
val resources = LocalResources.current
val density = LocalDensity.current
val haptic = LocalHapticFeedback.current
@@ -360,7 +341,7 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
id = u.id
title = u.longName
snippet =
resources.getString(
com.meshtastic.core.strings.getString(
Res.string.map_node_popup_details,
node.gpsString(),
formatAgo(node.lastHeard),
@@ -369,7 +350,11 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
)
ourNode?.distanceStr(node, displayUnits)?.let { dist ->
subDescription =
resources.getString(Res.string.map_subDescription, ourNode.bearing(node).toString(), dist)
com.meshtastic.core.strings.getString(
Res.string.map_subDescription,
ourNode.bearing(node).toString(),
dist,
)
}
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
position = nodePosition
@@ -390,16 +375,16 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
fun showDeleteMarkerDialog(waypoint: Waypoint) {
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(resources.getString(Res.string.waypoint_delete))
builder.setNeutralButton(resources.getString(Res.string.cancel)) { _, _ ->
builder.setTitle(com.meshtastic.core.strings.getString(Res.string.waypoint_delete))
builder.setNeutralButton(com.meshtastic.core.strings.getString(Res.string.cancel)) { _, _ ->
Timber.d("User canceled marker delete dialog")
}
builder.setNegativeButton(resources.getString(Res.string.delete_for_me)) { _, _ ->
builder.setNegativeButton(com.meshtastic.core.strings.getString(Res.string.delete_for_me)) { _, _ ->
Timber.d("User deleted waypoint ${waypoint.id} for me")
mapViewModel.deleteWaypoint(waypoint.id)
}
if (waypoint.lockedTo in setOf(0, mapViewModel.myNodeNum ?: 0) && isConnected) {
builder.setPositiveButton(resources.getString(Res.string.delete_for_everyone)) { _, _ ->
builder.setPositiveButton(com.meshtastic.core.strings.getString(Res.string.delete_for_everyone)) { _, _ ->
Timber.d("User deleted waypoint ${waypoint.id} for everyone")
mapViewModel.sendWaypoint(waypoint.copy { expire = 1 })
mapViewModel.deleteWaypoint(waypoint.id)
@@ -434,7 +419,7 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
}
fun getUsername(id: String?) = if (id == DataPacket.ID_LOCAL) {
resources.getString(Res.string.you)
com.meshtastic.core.strings.getString(Res.string.you)
} else {
mapViewModel.getUser(id).longName
}
@@ -485,30 +470,6 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
}
}
LaunchedEffect(showCurrentCacheInfo) {
if (!showCurrentCacheInfo) return@LaunchedEffect
context.showToast(Res.string.calculating)
val cacheManager = CacheManager(map)
val cacheCapacity = cacheManager.cacheCapacity()
val currentCacheUsage = cacheManager.currentCacheUsage()
val mapCacheInfoText =
getString(
Res.string.map_cache_info,
cacheCapacity / (1024.0 * 1024.0),
currentCacheUsage / (1024.0 * 1024.0),
)
MaterialAlertDialogBuilder(context)
.setTitle(resources.getString(Res.string.map_cache_manager))
.setMessage(mapCacheInfoText)
.setPositiveButton(resources.getString(Res.string.close)) { dialog, _ ->
showCurrentCacheInfo = false
dialog.dismiss()
}
.show()
}
val mapEventsReceiver =
object : MapEventsReceiver {
override fun singleTapConfirmedHelper(p: GeoPoint): Boolean {
@@ -564,7 +525,7 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
val tileCount: Int =
CacheManager(this)
.possibleTilesInArea(downloadRegionBoundingBox, zoomLevelMin.toInt(), zoomLevelMax.toInt())
cacheEstimate = resources.getString(Res.string.map_cache_tiles, tileCount)
cacheEstimate = com.meshtastic.core.strings.getString(Res.string.map_cache_tiles, tileCount)
}
val boxOverlayListener =
@@ -612,49 +573,9 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
}
}
fun showMapStyleDialog() {
val builder = MaterialAlertDialogBuilder(context)
val mapStyles: Array<CharSequence> = CustomTileSource.mTileSources.values.toTypedArray()
val mapStyleInt = mapViewModel.mapStyleId
builder.setSingleChoiceItems(mapStyles, mapStyleInt) { dialog, which ->
Timber.d("Set mapStyleId pref to $which")
mapViewModel.mapStyleId = which
dialog.dismiss()
map.setTileSource(loadOnlineTileSourceBase())
}
val dialog = builder.create()
dialog.show()
}
fun Context.showCacheManagerDialog() {
MaterialAlertDialogBuilder(this)
.setTitle(resources.getString(Res.string.map_offline_manager))
.setItems(
arrayOf<CharSequence>(
getString(Res.string.map_cache_size),
getString(Res.string.map_download_region),
getString(Res.string.map_clear_tiles),
getString(Res.string.cancel),
),
) { dialog, which ->
when (which) {
0 -> showCurrentCacheInfo = true
1 -> {
map.generateBoxOverlay()
dialog.dismiss()
}
2 -> purgeTileSource { scope.launch { context.showToast(it) } }
else -> dialog.dismiss()
}
}
.show()
}
Scaffold(
floatingActionButton = {
DownloadButton(showDownloadButton && downloadRegionBoundingBox == null) { context.showCacheManagerDialog() }
DownloadButton(showDownloadButton && downloadRegionBoundingBox == null) { showCacheManagerDialog = true }
},
) { innerPadding ->
Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
@@ -685,7 +606,7 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
MapButton(
onClick = ::showMapStyleDialog,
onClick = { showMapStyleDialog = true },
icon = Icons.Outlined.Layers,
contentDescription = Res.string.map_style_selection,
)
@@ -800,6 +721,44 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
}
}
if (showMapStyleDialog) {
MapStyleDialog(
selectedMapStyle = mapViewModel.mapStyleId,
onDismiss = { showMapStyleDialog = false },
onSelectMapStyle = {
mapViewModel.mapStyleId = it
map.setTileSource(loadOnlineTileSourceBase())
},
)
}
if (showCacheManagerDialog) {
CacheManagerDialog(
onClickOption = { option ->
when (option) {
CacheManagerOption.CurrentCacheSize -> {
scope.launch { context.showToast(Res.string.calculating) }
showCurrentCacheInfo = true
}
CacheManagerOption.DownloadRegion -> map.generateBoxOverlay()
CacheManagerOption.ClearTiles -> showPurgeTileSourceDialog = true
CacheManagerOption.Cancel -> Unit
}
showCacheManagerDialog = false
},
onDismiss = { showCacheManagerDialog = false },
)
}
if (showCurrentCacheInfo) {
CacheInfoDialog(mapView = map, onDismiss = { showCurrentCacheInfo = false })
}
if (showPurgeTileSourceDialog) {
PurgeTileSourceDialog(onDismiss = { showPurgeTileSourceDialog = false })
}
if (showEditWaypointDialog != null) {
EditWaypointDialog(
waypoint = showEditWaypointDialog ?: return, // Safe call
@@ -828,3 +787,159 @@ fun MapView(mapViewModel: MapViewModel = hiltViewModel(), navigateToNodeDetails:
)
}
}
@Composable
private fun MapStyleDialog(selectedMapStyle: Int, onDismiss: () -> Unit, onSelectMapStyle: (Int) -> Unit) {
val selected = remember { mutableStateOf(selectedMapStyle) }
MapsDialog(onDismiss = onDismiss) {
CustomTileSource.mTileSources.values.forEachIndexed { index, style ->
ListItem(
text = style,
trailingIcon = if (index == selected.value) Icons.Rounded.Check else null,
onClick = {
selected.value = index
onSelectMapStyle(index)
onDismiss()
},
)
}
}
}
private enum class CacheManagerOption(val label: StringResource) {
CurrentCacheSize(label = Res.string.map_cache_size),
DownloadRegion(label = Res.string.map_download_region),
ClearTiles(label = Res.string.map_clear_tiles),
Cancel(label = Res.string.cancel),
}
@Composable
private fun CacheManagerDialog(onClickOption: (CacheManagerOption) -> Unit, onDismiss: () -> Unit) {
MapsDialog(title = stringResource(Res.string.map_offline_manager), onDismiss = onDismiss) {
CacheManagerOption.entries.forEach { option ->
ListItem(text = stringResource(option.label), trailingIcon = null) {
onClickOption(option)
onDismiss()
}
}
}
}
@Composable
private fun CacheInfoDialog(mapView: MapView, onDismiss: () -> Unit) {
val (cacheCapacity, currentCacheUsage) =
remember(mapView) {
val cacheManager = CacheManager(mapView)
cacheManager.cacheCapacity() to cacheManager.currentCacheUsage()
}
MapsDialog(
title = stringResource(Res.string.map_cache_manager),
onDismiss = onDismiss,
negativeButton = { TextButton(onClick = { onDismiss() }) { Text(text = stringResource(Res.string.close)) } },
) {
Text(
modifier = Modifier.padding(16.dp),
text =
stringResource(
Res.string.map_cache_info,
cacheCapacity / (1024.0 * 1024.0),
currentCacheUsage / (1024.0 * 1024.0),
),
)
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun PurgeTileSourceDialog(onDismiss: () -> Unit) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val cache = SqlTileWriterExt()
val sourceList by derivedStateOf { cache.sources.map { it.source as String } }
val selected = remember { mutableStateListOf<Int>() }
MapsDialog(
title = stringResource(Res.string.map_tile_source),
positiveButton = {
TextButton(
enabled = selected.isNotEmpty(),
onClick = {
selected.forEach { selectedIndex ->
val source = sourceList[selectedIndex]
scope.launch {
context.showToast(
if (cache.purgeCache(source)) {
getString(Res.string.map_purge_success, source)
} else {
getString(Res.string.map_purge_fail)
},
)
}
}
onDismiss()
},
) {
Text(text = stringResource(Res.string.clear))
}
},
negativeButton = { TextButton(onClick = onDismiss) { Text(text = stringResource(Res.string.cancel)) } },
onDismiss = onDismiss,
) {
sourceList.forEachIndexed { index, source ->
val isSelected = selected.contains(index)
BasicListItem(
text = source,
trailingContent = { Checkbox(checked = isSelected, onCheckedChange = {}) },
onClick = {
if (isSelected) {
selected.remove(index)
} else {
selected.add(index)
}
},
) {}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MapsDialog(
title: String? = null,
onDismiss: () -> Unit,
positiveButton: (@Composable () -> Unit)? = null,
negativeButton: (@Composable () -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
BasicAlertDialog(onDismissRequest = onDismiss) {
Surface(
modifier = Modifier.wrapContentWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.large,
color = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
) {
Column {
title?.let {
Text(
modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 8.dp),
text = it,
style = MaterialTheme.typography.titleLarge,
)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { content() }
if (positiveButton != null || negativeButton != null) {
Row(Modifier.align(Alignment.End)) {
positiveButton?.invoke()
negativeButton?.invoke()
}
}
}
}
}
}

View File

@@ -29,7 +29,10 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -42,7 +45,13 @@ import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.Output
import androidx.compose.material.icons.rounded.WavingHand
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -53,16 +62,15 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.meshtastic.core.strings.getString
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.database.DatabaseConstants
@@ -95,7 +103,6 @@ import org.meshtastic.core.strings.theme_system
import org.meshtastic.core.ui.component.DropDownPreference
import org.meshtastic.core.ui.component.ListItem
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.component.MultipleChoiceAlertDialog
import org.meshtastic.core.ui.component.SwitchListItem
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
@@ -106,7 +113,7 @@ import org.meshtastic.feature.settings.radio.RadioConfigViewModel
import org.meshtastic.feature.settings.radio.component.EditDeviceProfileDialog
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
import org.meshtastic.feature.settings.util.LanguageUtils
import org.meshtastic.feature.settings.util.LanguageUtils.getLanguageMap
import org.meshtastic.feature.settings.util.LanguageUtils.languageMap
import org.meshtastic.proto.ClientOnlyProtos.DeviceProfile
import java.text.SimpleDateFormat
import java.util.Date
@@ -260,7 +267,6 @@ fun SettingsScreen(
onNavigate = onNavigate,
)
val scope = rememberCoroutineScope()
val context = LocalContext.current
TitledCard(title = stringResource(Res.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
@@ -470,39 +476,54 @@ private fun AppVersionButton(
@Composable
private fun LanguagePickerDialog(onDismiss: () -> Unit) {
val context = LocalContext.current
val choices = remember {
context
.getLanguageMap()
.map { (languageTag, languageName) -> languageName to { LanguageUtils.setAppLocale(languageTag) } }
.toMap()
SettingsDialog(title = stringResource(Res.string.preferences_language), onDismiss = onDismiss) {
languageMap().forEach { (languageTag, languageName) ->
ListItem(text = languageName, trailingIcon = null) {
LanguageUtils.setAppLocale(languageTag)
onDismiss()
}
}
}
}
MultipleChoiceAlertDialog(
title = stringResource(Res.string.preferences_language),
message = "",
choices = choices,
onDismissRequest = onDismiss,
)
private enum class ThemeOption(val label: StringResource, val mode: Int) {
DYNAMIC(label = Res.string.dynamic, mode = MODE_DYNAMIC),
LIGHT(label = Res.string.theme_light, mode = AppCompatDelegate.MODE_NIGHT_NO),
DARK(label = Res.string.theme_dark, mode = AppCompatDelegate.MODE_NIGHT_YES),
SYSTEM(label = Res.string.theme_system, mode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM),
}
@Composable
private fun ThemePickerDialog(onClickTheme: (Int) -> Unit, onDismiss: () -> Unit) {
val resources = LocalResources.current
val themeMap = remember {
mapOf(
resources.getString(Res.string.dynamic) to MODE_DYNAMIC,
resources.getString(Res.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO,
resources.getString(Res.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES,
resources.getString(Res.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
)
.mapValues { (_, value) -> { onClickTheme(value) } }
SettingsDialog(title = stringResource(Res.string.choose_theme), onDismiss = onDismiss) {
ThemeOption.entries.forEach { option ->
ListItem(text = stringResource(option.label), trailingIcon = null) {
onClickTheme(option.mode)
onDismiss()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SettingsDialog(title: String, onDismiss: () -> Unit, content: @Composable ColumnScope.() -> Unit) {
BasicAlertDialog(onDismissRequest = onDismiss) {
Surface(
modifier = Modifier.wrapContentWidth().wrapContentHeight(),
shape = MaterialTheme.shapes.large,
color = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
) {
Column {
Text(
modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 8.dp),
text = title,
style = MaterialTheme.typography.titleLarge,
)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { content() }
}
}
}
MultipleChoiceAlertDialog(
title = stringResource(Res.string.choose_theme),
message = "",
choices = themeMap,
onDismissRequest = onDismiss,
)
}

View File

@@ -17,10 +17,12 @@
package org.meshtastic.feature.settings.util
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalResources
import androidx.core.os.LocaleListCompat
import com.meshtastic.core.strings.getString
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.fr_HT
import org.meshtastic.core.strings.preferences_system_default
@@ -47,33 +49,38 @@ object LanguageUtils {
/** Using locales_config.xml, maps language tags to their localized language names (e.g.: "en" -> "English") */
@Suppress("CyclomaticComplexMethod")
fun Context.getLanguageMap(): Map<String, String> {
val languageTags = buildList {
add(SYSTEM_DEFAULT)
@Composable
fun languageMap(): Map<String, String> {
val resources = LocalResources.current
val languageTags =
remember(resources) {
buildList {
add(SYSTEM_DEFAULT)
try {
resources.getXml(org.meshtastic.feature.settings.R.xml.locales_config).use { parser ->
while (parser.eventType != XmlPullParser.END_DOCUMENT) {
if (parser.eventType == XmlPullParser.START_TAG && parser.name == "locale") {
val languageTag =
parser.getAttributeValue("http://schemas.android.com/apk/res/android", "name")
languageTag?.let { add(it) }
try {
resources.getXml(org.meshtastic.feature.settings.R.xml.locales_config).use { parser ->
while (parser.eventType != XmlPullParser.END_DOCUMENT) {
if (parser.eventType == XmlPullParser.START_TAG && parser.name == "locale") {
val languageTag =
parser.getAttributeValue("http://schemas.android.com/apk/res/android", "name")
languageTag?.let { add(it) }
}
parser.next()
}
}
parser.next()
} catch (e: Exception) {
Timber.e("Error parsing locale_config.xml: ${e.message}")
}
}
} catch (e: Exception) {
Timber.e("Error parsing locale_config.xml: ${e.message}")
}
}
return languageTags.associateWith { languageTag ->
when (languageTag) {
SYSTEM_DEFAULT -> getString(Res.string.preferences_system_default)
"fr-HT" -> getString(Res.string.fr_HT)
"pt-BR" -> getString(Res.string.pt_BR)
"zh-CN" -> getString(Res.string.zh_CN)
"zh-TW" -> getString(Res.string.zh_TW)
SYSTEM_DEFAULT -> stringResource(Res.string.preferences_system_default)
"fr-HT" -> stringResource(Res.string.fr_HT)
"pt-BR" -> stringResource(Res.string.pt_BR)
"zh-CN" -> stringResource(Res.string.zh_CN)
"zh-TW" -> stringResource(Res.string.zh_TW)
else -> {
Locale.forLanguageTag(languageTag).let { locale ->
locale.getDisplayLanguage(locale).replaceFirstChar { char ->