mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Kmp strings cleanup (#3669)
This commit is contained in:
parent
bde7c47931
commit
57ef889caa
6 changed files with 341 additions and 209 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue