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