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 }