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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 341 additions and 209 deletions

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 ->