AlertDialogs.kt (28368B) - raw


      1 package me.rhunk.snapenhance.ui.util
      2 
      3 import android.content.Context
      4 import android.view.MotionEvent
      5 import android.widget.Toast
      6 import androidx.compose.foundation.ScrollState
      7 import androidx.compose.foundation.background
      8 import androidx.compose.foundation.clickable
      9 import androidx.compose.foundation.layout.*
     10 import androidx.compose.foundation.shape.RoundedCornerShape
     11 import androidx.compose.foundation.text.KeyboardOptions
     12 import androidx.compose.foundation.verticalScroll
     13 import androidx.compose.material.icons.Icons
     14 import androidx.compose.material.icons.filled.Check
     15 import androidx.compose.material.icons.filled.DeleteOutline
     16 import androidx.compose.material.icons.filled.Edit
     17 import androidx.compose.material.icons.filled.Save
     18 import androidx.compose.material3.*
     19 import androidx.compose.runtime.*
     20 import androidx.compose.ui.Alignment
     21 import androidx.compose.ui.Modifier
     22 import androidx.compose.ui.draw.clip
     23 import androidx.compose.ui.draw.clipToBounds
     24 import androidx.compose.ui.focus.FocusRequester
     25 import androidx.compose.ui.focus.focusRequester
     26 import androidx.compose.ui.graphics.Color
     27 import androidx.compose.ui.graphics.toArgb
     28 import androidx.compose.ui.layout.onGloballyPositioned
     29 import androidx.compose.ui.platform.LocalContext
     30 import androidx.compose.ui.text.TextRange
     31 import androidx.compose.ui.text.font.FontWeight
     32 import androidx.compose.ui.text.input.ImeAction
     33 import androidx.compose.ui.text.input.KeyboardType
     34 import androidx.compose.ui.text.input.TextFieldValue
     35 import androidx.compose.ui.unit.dp
     36 import androidx.compose.ui.unit.sp
     37 import androidx.compose.ui.viewinterop.AndroidView
     38 import androidx.core.net.toUri
     39 import com.github.skydoves.colorpicker.compose.*
     40 import com.google.gson.JsonParser
     41 import kotlinx.coroutines.Dispatchers
     42 import kotlinx.coroutines.delay
     43 import kotlinx.coroutines.launch
     44 import me.rhunk.snapenhance.common.Constants
     45 import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper
     46 import me.rhunk.snapenhance.common.config.DataProcessors
     47 import me.rhunk.snapenhance.common.config.PropertyPair
     48 import me.rhunk.snapenhance.common.ui.AutoClearKeyboardFocus
     49 import me.rhunk.snapenhance.common.util.ktx.await
     50 import okhttp3.OkHttpClient
     51 import okhttp3.Request
     52 import org.osmdroid.config.Configuration
     53 import org.osmdroid.tileprovider.tilesource.TileSourceFactory
     54 import org.osmdroid.util.GeoPoint
     55 import org.osmdroid.views.CustomZoomButtonsController
     56 import org.osmdroid.views.MapView
     57 import org.osmdroid.views.overlay.Marker
     58 import org.osmdroid.views.overlay.Overlay
     59 import java.io.File
     60 
     61 
     62 class AlertDialogs(
     63     private val translation: LocaleWrapper,
     64 ){
     65     @Composable
     66     fun DefaultDialogCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
     67         Card(
     68             shape = MaterialTheme.shapes.large,
     69             modifier = Modifier
     70                 .padding(16.dp)
     71                 .then(modifier),
     72         ) {
     73             Column(
     74                 modifier = Modifier
     75                     .padding(10.dp, 10.dp, 10.dp, 10.dp)
     76                     .verticalScroll(ScrollState(0)),
     77             ) { content() }
     78         }
     79     }
     80 
     81     @Composable
     82     fun ConfirmDialog(
     83         title: String,
     84         message: String? = null,
     85         onConfirm: () -> Unit,
     86         onDismiss: () -> Unit,
     87     ) {
     88         DefaultDialogCard {
     89             Text(
     90                 text = title,
     91                 fontSize = 20.sp,
     92                 fontWeight = FontWeight.Bold,
     93                 modifier = Modifier.padding(start = 5.dp, bottom = 10.dp)
     94             )
     95             if (message != null) {
     96                 Text(
     97                     text = message,
     98                     style = MaterialTheme.typography.bodyMedium,
     99                     modifier = Modifier.padding(bottom = 15.dp)
    100                 )
    101             }
    102             Row(
    103                 modifier = Modifier.fillMaxWidth(),
    104                 horizontalArrangement = Arrangement.SpaceEvenly,
    105             ) {
    106                 Button(onClick = { onDismiss() }) {
    107                     Text(text = translation["button.cancel"])
    108                 }
    109                 Button(onClick = { onConfirm() }) {
    110                     Text(text = translation["button.ok"])
    111                 }
    112             }
    113         }
    114     }
    115 
    116     @Composable
    117     fun InfoDialog(
    118         title: String,
    119         message: String? = null,
    120         onDismiss: () -> Unit,
    121     ) {
    122         DefaultDialogCard {
    123             Text(
    124                 text = title,
    125                 fontSize = 20.sp,
    126                 modifier = Modifier.padding(start = 5.dp, bottom = 10.dp)
    127             )
    128             if (message != null) {
    129                 Text(
    130                     text = message,
    131                     style = MaterialTheme.typography.bodySmall,
    132                     modifier = Modifier.padding(bottom = 15.dp)
    133                 )
    134             }
    135             Row(
    136                 modifier = Modifier.fillMaxWidth(),
    137                 horizontalArrangement = Arrangement.SpaceEvenly,
    138             ) {
    139                 Button(onClick = { onDismiss() }) {
    140                     Text(text = translation["button.ok"])
    141                 }
    142             }
    143         }
    144     }
    145 
    146     @Composable
    147     fun TranslatedText(property: PropertyPair<*>, key: String, modifier: Modifier = Modifier) {
    148         Text(
    149             text = property.key.propertyOption(translation, key),
    150             modifier = Modifier
    151                 .padding(10.dp, 10.dp, 10.dp, 10.dp)
    152                 .then(modifier)
    153         )
    154     }
    155 
    156     @Composable
    157     @Suppress("UNCHECKED_CAST")
    158     fun UniqueSelectionDialog(property: PropertyPair<*>) {
    159         val keys = (property.value.defaultValues as List<String>).toMutableList().apply {
    160             add(0, "null")
    161         }
    162 
    163         var selectedValue by remember {
    164             mutableStateOf(property.value.getNullable()?.toString() ?: "null")
    165         }
    166 
    167         DefaultDialogCard {
    168             keys.forEachIndexed { index, item ->
    169                 fun select() {
    170                     selectedValue = item
    171                     property.value.setAny(if (index == 0) {
    172                         null
    173                     } else {
    174                         item
    175                     })
    176                 }
    177 
    178                 Row(
    179                     modifier = Modifier.clickable { select() },
    180                     verticalAlignment = Alignment.CenterVertically
    181                 ) {
    182                     TranslatedText(
    183                         property = property,
    184                         key = item,
    185                         modifier = Modifier.weight(1f)
    186                     )
    187                     RadioButton(
    188                         selected = selectedValue == item,
    189                         onClick = { select() }
    190                     )
    191                 }
    192             }
    193         }
    194     }
    195 
    196     @Composable
    197     fun KeyboardInputDialog(property: PropertyPair<*>, dismiss: () -> Unit = {}) {
    198         val focusRequester = remember { FocusRequester() }
    199         val context = LocalContext.current
    200 
    201         DefaultDialogCard {
    202             var fieldValue by remember {
    203                 mutableStateOf(property.value.get().toString().let {
    204                     TextFieldValue(
    205                         text = it,
    206                         selection = TextRange(it.length)
    207                     )
    208                 })
    209             }
    210 
    211             TextField(
    212                 modifier = Modifier
    213                     .fillMaxWidth()
    214                     .padding(all = 10.dp)
    215                     .onGloballyPositioned {
    216                         focusRequester.requestFocus()
    217                     }
    218                     .focusRequester(focusRequester),
    219                 value = fieldValue,
    220                 onValueChange = { fieldValue = it },
    221                 keyboardOptions = when (property.key.dataType.type) {
    222                     DataProcessors.Type.INTEGER -> KeyboardOptions(keyboardType = KeyboardType.Number)
    223                     DataProcessors.Type.FLOAT -> KeyboardOptions(keyboardType = KeyboardType.Decimal)
    224                     else -> KeyboardOptions(keyboardType = KeyboardType.Text)
    225                 },
    226                 singleLine = true
    227             )
    228 
    229             Row(
    230                 modifier = Modifier
    231                     .padding(top = 10.dp)
    232                     .fillMaxWidth(),
    233                 horizontalArrangement = Arrangement.SpaceEvenly,
    234             ) {
    235                 Button(onClick = { dismiss() }) {
    236                     Text(text = translation["button.cancel"])
    237                 }
    238                 Button(onClick = {
    239                     if (fieldValue.text.isNotEmpty() && property.key.params.inputCheck?.invoke(fieldValue.text) == false) {
    240                         Toast.makeText(context, "Invalid input! Make sure you entered a valid value.", Toast.LENGTH_SHORT).show() //TODO: i18n
    241                         return@Button
    242                     }
    243 
    244                     when (property.key.dataType.type) {
    245                         DataProcessors.Type.INTEGER -> {
    246                             runCatching {
    247                                 property.value.setAny(fieldValue.text.toInt())
    248                             }.onFailure {
    249                                 property.value.setAny(0)
    250                             }
    251                         }
    252                         DataProcessors.Type.FLOAT -> {
    253                             runCatching {
    254                                 property.value.setAny(fieldValue.text.toFloat())
    255                             }.onFailure {
    256                                 property.value.setAny(0f)
    257                             }
    258                         }
    259                         else -> property.value.setAny(fieldValue.text)
    260                     }
    261                     dismiss()
    262                 }) {
    263                     Text(text = translation["button.ok"])
    264                 }
    265             }
    266         }
    267     }
    268 
    269     @Composable
    270     fun RawInputDialog(onDismiss: () -> Unit, onConfirm: (value: String) -> Unit) {
    271         val focusRequester = remember { FocusRequester() }
    272 
    273         DefaultDialogCard {
    274             val fieldValue = remember {
    275                 mutableStateOf(TextFieldValue())
    276             }
    277 
    278             TextField(
    279                 modifier = Modifier
    280                     .fillMaxWidth()
    281                     .padding(all = 10.dp)
    282                     .onGloballyPositioned {
    283                         focusRequester.requestFocus()
    284                     }
    285                     .focusRequester(focusRequester),
    286                 value = fieldValue.value,
    287                 onValueChange = {
    288                     fieldValue.value = it
    289                 },
    290                 singleLine = true
    291             )
    292 
    293             Row(
    294                 modifier = Modifier
    295                     .padding(top = 10.dp)
    296                     .fillMaxWidth(),
    297                 horizontalArrangement = Arrangement.SpaceEvenly,
    298             ) {
    299                 Button(onClick = { onDismiss() }) {
    300                     Text(text = translation["button.cancel"])
    301                 }
    302                 Button(onClick = {
    303                     onConfirm(fieldValue.value.text)
    304                 }) {
    305                     Text(text = translation["button.ok"])
    306                 }
    307             }
    308         }
    309     }
    310 
    311     @Composable
    312     @Suppress("UNCHECKED_CAST")
    313     fun MultipleSelectionDialog(property: PropertyPair<*>) {
    314         val defaultItems = property.value.defaultValues as List<String>
    315         val toggledStates = property.value.get() as MutableList<String>
    316         DefaultDialogCard {
    317             defaultItems.forEach { key ->
    318                 var state by remember { mutableStateOf(toggledStates.contains(key)) }
    319 
    320                 fun toggle(value: Boolean? = null) {
    321                     state = value ?: !state
    322                     if (state) {
    323                         toggledStates.add(key)
    324                     } else {
    325                         toggledStates.remove(key)
    326                     }
    327                 }
    328 
    329                 Row(
    330                     modifier = Modifier.clickable { toggle() },
    331                     verticalAlignment = Alignment.CenterVertically
    332                 ) {
    333                     TranslatedText(
    334                         property = property,
    335                         key = key,
    336                         modifier = Modifier
    337                             .weight(1f)
    338                     )
    339                     Switch(
    340                         checked = state,
    341                         onCheckedChange = {
    342                             toggle(it)
    343                         }
    344                     )
    345                 }
    346             }
    347         }
    348     }
    349 
    350     @Composable
    351     fun ColorPickerDialog(
    352         initialColor: Color?,
    353         setProperty: (Color?) -> Unit,
    354         dismiss: () -> Unit
    355     ) {
    356         var currentColor by remember { mutableStateOf(initialColor) }
    357 
    358         DefaultDialogCard {
    359             val controller = remember { ColorPickerController().apply {
    360                 if (currentColor == null) {
    361                     setWheelAlpha(1f)
    362                     setBrightness(1f, false)
    363                 }
    364             } }
    365             var colorHexValue by remember {
    366                 mutableStateOf(currentColor?.toArgb()?.let { Integer.toHexString(it) } ?: "")
    367             }
    368 
    369             Box(
    370                 modifier = Modifier.fillMaxWidth(),
    371                 contentAlignment = Alignment.Center,
    372             ) {
    373                 TextField(
    374                     value = colorHexValue,
    375                     onValueChange = { value ->
    376                         colorHexValue = value
    377                         runCatching {
    378                             currentColor = Color(android.graphics.Color.parseColor("#$value")).also {
    379                                 controller.selectByColor(it, true)
    380                                 setProperty(it)
    381                             }
    382                         }.onFailure {
    383                             currentColor = null
    384                         }
    385                     },
    386                     label = { Text(text = "Hex Color") },
    387                     modifier = Modifier
    388                         .fillMaxWidth()
    389                         .padding(10.dp),
    390                     singleLine = true,
    391                     colors = TextFieldDefaults.colors(
    392                         unfocusedContainerColor = Color.Transparent,
    393                         focusedContainerColor = Color.Transparent,
    394                     )
    395                 )
    396             }
    397             HsvColorPicker(
    398                 modifier = Modifier
    399                     .fillMaxWidth()
    400                     .height(300.dp)
    401                     .padding(10.dp),
    402                 initialColor = remember { currentColor },
    403                 controller = controller,
    404                 onColorChanged = {
    405                     if (!it.fromUser) return@HsvColorPicker
    406                     currentColor = it.color
    407                     colorHexValue = Integer.toHexString(it.color.toArgb())
    408                     setProperty(it.color)
    409                 }
    410             )
    411             AlphaSlider(
    412                 modifier = Modifier
    413                     .fillMaxWidth()
    414                     .padding(10.dp)
    415                     .height(35.dp),
    416                 initialColor = remember { currentColor },
    417                 controller = controller,
    418             )
    419             BrightnessSlider(
    420                 modifier = Modifier
    421                     .fillMaxWidth()
    422                     .padding(10.dp)
    423                     .height(35.dp),
    424                 initialColor = remember { currentColor },
    425                 controller = controller,
    426             )
    427             Row(
    428                 modifier = Modifier
    429                     .fillMaxWidth()
    430                     .padding(5.dp),
    431                 horizontalArrangement = Arrangement.SpaceEvenly,
    432                 verticalAlignment = Alignment.CenterVertically,
    433             ) {
    434                 AlphaTile(
    435                     modifier = Modifier
    436                         .size(80.dp)
    437                         .clip(RoundedCornerShape(6.dp)),
    438                     controller = controller
    439                 )
    440                 IconButton(onClick = {
    441                     setProperty(null)
    442                     dismiss()
    443                 }) {
    444                     Icon(
    445                         modifier = Modifier.size(60.dp),
    446                         imageVector = Icons.Filled.DeleteOutline,
    447                         contentDescription = null
    448                     )
    449                 }
    450             }
    451         }
    452     }
    453 
    454     @Composable
    455     fun ColorPickerPropertyDialog(
    456         property: PropertyPair<*>,
    457         dismiss: () -> Unit = {},
    458     ) {
    459         var currentColor by remember {
    460             mutableStateOf((property.value.getNullable() as? Int)?.let { Color(it) })
    461         }
    462 
    463         ColorPickerDialog(
    464             initialColor = currentColor,
    465             setProperty = setProperty@{
    466                 currentColor = it
    467                 property.value.setAny(it?.toArgb())
    468                 if (it == null) {
    469                     property.value.setAny(property.value.defaultValues?.firstOrNull() ?: return@setProperty)
    470                 }
    471             },
    472             dismiss = dismiss
    473         )
    474     }
    475 
    476     @Composable
    477     fun ChooseLocationDialog(
    478         property: PropertyPair<*>,
    479         marker: MutableState<Marker?> = remember { mutableStateOf(null) },
    480         mapView: MutableState<MapView?> = remember { mutableStateOf(null) },
    481         saveCoordinates: (() -> Unit)? = null,
    482         dismiss: () -> Unit = {}
    483     ) {
    484         val coordinates = remember {
    485             (property.value.get() as Pair<*, *>).let {
    486                 it.first.toString().toDouble() to it.second.toString().toDouble()
    487             }
    488         }
    489         val context = LocalContext.current
    490 
    491         mapView.value = remember {
    492             Configuration.getInstance().apply {
    493                 osmdroidBasePath = File(context.cacheDir, "osmdroid")
    494                 load(context, context.getSharedPreferences("osmdroid", Context.MODE_PRIVATE))
    495             }
    496             MapView(context).apply {
    497                 setMultiTouchControls(true)
    498                 zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
    499                 setTileSource(TileSourceFactory.MAPNIK)
    500 
    501                 val startPoint = GeoPoint(coordinates.first, coordinates.second)
    502                 controller.setZoom(10.0)
    503                 controller.setCenter(startPoint)
    504 
    505                 marker.value = Marker(this).apply {
    506                     isDraggable = true
    507                     position = startPoint
    508                     setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
    509                 }
    510 
    511                 overlays.add(object: Overlay() {
    512                     override fun onSingleTapConfirmed(e: MotionEvent, mapView: MapView): Boolean {
    513                         marker.value?.position = mapView.projection.fromPixels(e.x.toInt(), e.y.toInt()) as GeoPoint
    514                         mapView.invalidate()
    515                         return true
    516                     }
    517                 })
    518 
    519                 overlays.add(marker.value)
    520             }
    521         }
    522 
    523         DisposableEffect(Unit) {
    524             onDispose {
    525                 mapView.value?.onDetach()
    526             }
    527         }
    528 
    529         var customCoordinatesDialog by remember { mutableStateOf(false) }
    530 
    531 
    532         val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
    533         val okHttpClient by lazy { OkHttpClient() }
    534 
    535         Box(
    536             modifier = Modifier
    537                 .fillMaxWidth()
    538                 .clipToBounds()
    539                 .fillMaxHeight(fraction = 0.9f),
    540         ) {
    541             AndroidView(
    542                 factory = { mapView.value!! },
    543             )
    544             Column(
    545                 modifier = Modifier
    546                 .align(Alignment.TopCenter)
    547                     .fillMaxWidth()
    548             ) {
    549                 var locationName by remember { mutableStateOf<String>("") }
    550                 var addressResults by remember { mutableStateOf<List<Triple<String, String, String>>>(emptyList()) }
    551                 var searchJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }
    552 
    553                 suspend fun search() {
    554                     okHttpClient.newCall(Request.Builder()
    555                         .url("https://nominatim.openstreetmap.org/search".toUri().buildUpon().appendQueryParameter("q", locationName).appendQueryParameter("format", "jsonv2").build().toString())
    556                         .header("User-Agent", Constants.USER_AGENT)
    557                         .build()
    558                     ).await().use { response ->
    559                         if (!response.isSuccessful) {
    560                             return@use
    561                         }
    562 
    563                         runCatching {
    564                             val body = JsonParser.parseString(response.body.string()).asJsonArray
    565                             addressResults = body.take(5).map { jsonElement ->
    566                                 val jsonObject = jsonElement.asJsonObject
    567                                 Triple(
    568                                     jsonObject.get("display_name").asString,
    569                                     jsonObject.get("lat").asString,
    570                                     jsonObject.get("lon").asString
    571                                 )
    572                             }
    573                         }
    574                     }
    575 
    576                     searchJob = null
    577                 }
    578 
    579                 TextField(
    580                     modifier = Modifier
    581                         .fillMaxWidth(),
    582                     value = locationName,
    583                     onValueChange = {
    584                         locationName = it.replace("\n", "")
    585                         if (locationName == "") {
    586                             addressResults = emptyList()
    587                             searchJob?.cancel()
    588                             searchJob = null
    589                             return@TextField
    590                         }
    591                         searchJob?.cancel()
    592                         searchJob = coroutineScope.launch {
    593                             delay(500)
    594                             search()
    595                         }
    596                     },
    597                     label = { Text(text = "Search") },
    598                     singleLine = true,
    599                     keyboardOptions = KeyboardOptions(imeAction = ImeAction.None)
    600                 )
    601 
    602                 AutoClearKeyboardFocus(onFocusClear = {
    603                     locationName = ""
    604                     addressResults = emptyList()
    605                     searchJob?.cancel()
    606                     searchJob = null
    607                 })
    608 
    609                 Column(
    610                     modifier = Modifier
    611                         .fillMaxWidth()
    612                         .background(MaterialTheme.colorScheme.background)
    613                         .verticalScroll(ScrollState(0)),
    614                 ) {
    615                     if (addressResults.isNotEmpty()) {
    616                             addressResults.forEach { address ->
    617                                 Row(
    618                                     modifier = Modifier
    619                                         .fillMaxWidth()
    620                                         .clickable {
    621                                             marker.value?.position = GeoPoint(address.second.toDouble(), address.third.toDouble())
    622                                             mapView.value?.controller?.setCenter(marker.value?.position)
    623                                             mapView.value?.invalidate()
    624                                         }
    625                                 ) {
    626                                     Text(
    627                                         text = address.first,
    628                                         modifier = Modifier
    629                                             .padding(10.dp)
    630                                             .fillMaxWidth(),
    631                                     )
    632                                 }
    633                             }
    634                     } else {
    635                         if (searchJob?.isActive == true) {
    636                             Row(
    637                                 modifier = Modifier.fillMaxWidth().padding(10.dp),
    638                                 horizontalArrangement = Arrangement.Center
    639                             ) {
    640                                 CircularProgressIndicator()
    641                             }
    642                         }
    643                     }
    644                 }
    645             }
    646 
    647 
    648             Row(
    649                 modifier = Modifier
    650                     .align(Alignment.BottomEnd)
    651                     .padding(10.dp),
    652                 horizontalArrangement = Arrangement.spacedBy(10.dp),
    653             ) {
    654                 FilledIconButton(
    655                     onClick = {
    656                         val lat = marker.value?.position?.latitude ?: coordinates.first
    657                         val lon = marker.value?.position?.longitude ?: coordinates.second
    658                         property.value.setAny(lat to lon)
    659                         dismiss()
    660                     }) {
    661                     Icon(
    662                         modifier = Modifier
    663                             .size(60.dp)
    664                             .padding(5.dp),
    665                         imageVector = Icons.Filled.Check,
    666                         contentDescription = null
    667                     )
    668                 }
    669                 saveCoordinates?.let {
    670                     FilledIconButton(
    671                         onClick = { it() }) {
    672                         Icon(
    673                             modifier = Modifier
    674                                 .size(60.dp)
    675                                 .padding(5.dp),
    676                             imageVector = Icons.Filled.Save,
    677                             contentDescription = null
    678                         )
    679                     }
    680                 }
    681 
    682                 FilledIconButton(
    683                     onClick = {
    684                         customCoordinatesDialog = true
    685                     }) {
    686                     Icon(
    687                         modifier = Modifier
    688                             .size(60.dp)
    689                             .padding(5.dp),
    690                         imageVector = Icons.Filled.Edit,
    691                         contentDescription = null
    692                     )
    693                 }
    694             }
    695 
    696             if (customCoordinatesDialog) {
    697                 val lat = remember { mutableStateOf(coordinates.first.toString()) }
    698                 val lon = remember { mutableStateOf(coordinates.second.toString()) }
    699 
    700                 Dialog(onDismissRequest = {
    701                     customCoordinatesDialog = false
    702                 }) {
    703                     DefaultDialogCard(
    704                         modifier = Modifier.align(Alignment.Center)
    705                     ) {
    706                         TextField(
    707                             modifier = Modifier
    708                                 .fillMaxWidth()
    709                                 .padding(all = 10.dp),
    710                             value = lat.value,
    711                             onValueChange = { lat.value = it },
    712                             label = { Text(text = "Latitude") },
    713                             singleLine = true
    714                         )
    715                         TextField(
    716                             modifier = Modifier
    717                                 .fillMaxWidth()
    718                                 .padding(all = 10.dp),
    719                             value = lon.value,
    720                             onValueChange = { lon.value = it },
    721                             label = { Text(text = "Longitude") },
    722                             singleLine = true
    723                         )
    724                         Row(
    725                             modifier = Modifier.fillMaxWidth(),
    726                             horizontalArrangement = Arrangement.SpaceEvenly,
    727                         ) {
    728                             Button(onClick = {
    729                                 customCoordinatesDialog = false
    730                             }) {
    731                                 Text(text = translation["button.cancel"])
    732                             }
    733 
    734                             Button(onClick = {
    735                                 marker.value?.position = GeoPoint(lat.value.toDouble(), lon.value.toDouble())
    736                                 mapView.value?.controller?.setCenter(marker.value?.position)
    737                                 mapView.value?.invalidate()
    738                                 customCoordinatesDialog = false
    739                             }) {
    740                                 Text(text = translation["button.ok"])
    741                             }
    742                         }
    743                     }
    744                 }
    745             }
    746         }
    747     }
    748 }