BetterLocationRoot.kt (20820B) - raw


      1 package me.rhunk.snapenhance.ui.manager.pages.location
      2 
      3 import android.os.Parcel
      4 import androidx.compose.foundation.layout.*
      5 import androidx.compose.foundation.lazy.LazyColumn
      6 import androidx.compose.foundation.lazy.items
      7 import androidx.compose.material.icons.Icons
      8 import androidx.compose.material.icons.filled.Add
      9 import androidx.compose.material.icons.filled.DeleteOutline
     10 import androidx.compose.material.icons.filled.Edit
     11 import androidx.compose.material3.*
     12 import androidx.compose.runtime.*
     13 import androidx.compose.ui.Alignment
     14 import androidx.compose.ui.Modifier
     15 import androidx.compose.ui.draw.clipToBounds
     16 import androidx.compose.ui.text.font.FontWeight
     17 import androidx.compose.ui.text.style.TextAlign
     18 import androidx.compose.ui.text.style.TextOverflow
     19 import androidx.compose.ui.unit.dp
     20 import androidx.compose.ui.unit.sp
     21 import androidx.navigation.NavBackStackEntry
     22 import kotlinx.coroutines.Dispatchers
     23 import kotlinx.coroutines.launch
     24 import kotlinx.coroutines.withContext
     25 import me.rhunk.snapenhance.bridge.location.FriendLocation
     26 import me.rhunk.snapenhance.bridge.location.LocationCoordinates
     27 import me.rhunk.snapenhance.common.ui.rememberAsyncMutableStateList
     28 import me.rhunk.snapenhance.common.ui.rememberAsyncUpdateDispatcher
     29 import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
     30 import me.rhunk.snapenhance.storage.addOrUpdateLocationCoordinate
     31 import me.rhunk.snapenhance.storage.getLocationCoordinates
     32 import me.rhunk.snapenhance.storage.removeLocationCoordinate
     33 import me.rhunk.snapenhance.ui.manager.Routes
     34 import me.rhunk.snapenhance.ui.util.AlertDialogs
     35 import me.rhunk.snapenhance.ui.util.DialogProperties
     36 import me.rhunk.snapenhance.ui.util.coil.BitmojiImage
     37 import org.osmdroid.util.GeoPoint
     38 import org.osmdroid.views.MapView
     39 import org.osmdroid.views.overlay.Marker
     40 
     41 class BetterLocationRoot : Routes.Route() {
     42     private val alertDialogs by lazy { AlertDialogs(context.translation) }
     43 
     44     @Composable
     45     private fun FriendLocationItem(
     46         friendLocation: FriendLocation,
     47         dismiss: () -> Unit
     48     ) {
     49         ElevatedCard(onClick = {
     50             context.config.root.global.betterLocation.coordinates.setAny(friendLocation.latitude to friendLocation.longitude)
     51             dismiss()
     52         }, modifier = Modifier.padding(4.dp)) {
     53             Row(
     54                 modifier = Modifier
     55                     .padding(8.dp)
     56                     .fillMaxWidth(),
     57                 verticalAlignment = Alignment.CenterVertically
     58             ) {
     59                 BitmojiImage(
     60                     context = context,
     61                     url = BitmojiSelfie.getBitmojiSelfie(
     62                         friendLocation.bitmojiSelfieId,
     63                         friendLocation.bitmojiId,
     64                         BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D
     65                     ),
     66                     size = 48,
     67                     modifier = Modifier.padding(6.dp)
     68                 )
     69                 Column(
     70                     modifier = Modifier.weight(1f),
     71                 ) {
     72                     Text(friendLocation.displayName?.let { "$it (${friendLocation.username})" }
     73                         ?: friendLocation.username, fontSize = 16.sp, fontWeight = FontWeight.Bold)
     74                     Text(
     75                         text = buildString {
     76                             append(friendLocation.localityPieces.joinToString(", "))
     77                             append("\n")
     78                             append("Lat: ${friendLocation.latitude.toFloat()}, Lng: ${friendLocation.longitude.toFloat()}")
     79                         },
     80                         fontSize = 10.sp,
     81                         fontWeight = FontWeight.Light,
     82                         lineHeight = 15.sp
     83                     )
     84                 }
     85             }
     86         }
     87     }
     88 
     89     @Composable
     90     private fun FriendLocationsDialogs(
     91         friendsLocation: List<FriendLocation>,
     92         dismiss: () -> Unit
     93     ) {
     94         var search by remember { mutableStateOf("") }
     95         val filteredFriendsLocation = rememberAsyncMutableStateList(defaultValue = friendsLocation, keys = arrayOf(search)) {
     96             search.takeIf { it.isNotBlank() }?.let {
     97                 friendsLocation.filter {
     98                     it.displayName?.contains(search, ignoreCase = true) == true || it.username.contains(search, ignoreCase = true)
     99                 }
    100             }  ?: friendsLocation
    101         }
    102 
    103         ElevatedCard(
    104             shape = MaterialTheme.shapes.large,
    105             modifier = Modifier.padding(top = 32.dp, bottom = 32.dp)
    106         ) {
    107             Text(
    108                 translation["teleport_to_friend_title"],
    109                 fontSize = 20.sp,
    110                 fontWeight = FontWeight.Bold,
    111                 textAlign = TextAlign.Center,
    112                 modifier = Modifier
    113                     .fillMaxWidth()
    114                     .padding(12.dp)
    115             )
    116             OutlinedTextField(
    117                 modifier = Modifier
    118                     .fillMaxWidth()
    119                     .padding(8.dp),
    120                 value = search,
    121                 onValueChange = { search = it },
    122                 label = { Text(translation["search_bar"]) }
    123             )
    124             LazyColumn(
    125                 modifier = Modifier
    126                     .fillMaxSize()
    127             ) {
    128                 item {
    129                     if (friendsLocation.isEmpty()) {
    130                         Text(
    131                             translation["no_friends_map"],
    132                             fontSize = 16.sp,
    133                             modifier = Modifier.padding(16.dp),
    134                             fontWeight = FontWeight.Light
    135                         )
    136                     } else if (filteredFriendsLocation.isEmpty()) {
    137                         Text(
    138                             translation["no_friends_found"],
    139                             fontSize = 16.sp,
    140                             modifier = Modifier.padding(16.dp),
    141                             fontWeight = FontWeight.Light
    142                         )
    143                     }
    144                 }
    145                 items(filteredFriendsLocation) { friendLocation ->
    146                     FriendLocationItem(friendLocation, dismiss)
    147                 }
    148             }
    149         }
    150     }
    151 
    152     override val content: @Composable (NavBackStackEntry) -> Unit = {
    153         val coordinatesProperty = remember {
    154             context.config.root.global.betterLocation.getPropertyPair("coordinates")
    155         }
    156 
    157         val updateDispatcher = rememberAsyncUpdateDispatcher()
    158         val savedCoordinates = rememberAsyncMutableStateList(
    159             defaultValue = listOf(),
    160             updateDispatcher = updateDispatcher
    161         ) {
    162             context.database.getLocationCoordinates()
    163         }
    164         var showMap by remember { mutableStateOf(false) }
    165         var addSavedCoordinateDialog by remember { mutableStateOf(false) }
    166         var showTeleportDialog by remember { mutableStateOf(false) }
    167 
    168         val marker = remember { mutableStateOf<Marker?>(null) }
    169         val mapView = remember { mutableStateOf<MapView?>(null) }
    170         var spoofedCoordinates by remember(showTeleportDialog, showMap) { mutableStateOf(coordinatesProperty.value.get() as? Pair<*, *>) }
    171 
    172         fun addSavedCoordinate(id: Int?, locationCoordinates: LocationCoordinates, onSuccess: suspend (id: Int) -> Unit = {}) {
    173             context.coroutineScope.launch {
    174                 onSuccess(context.database.addOrUpdateLocationCoordinate(id, locationCoordinates))
    175             }
    176         }
    177 
    178         if (showTeleportDialog) {
    179             me.rhunk.snapenhance.ui.util.Dialog(
    180                 properties = DialogProperties(usePlatformDefaultWidth = false),
    181                 onDismissRequest = { showTeleportDialog = false },
    182                 content = {
    183                     FriendLocationsDialogs(remember { context.locationManager.friendsLocation }) {
    184                         showTeleportDialog = false
    185                         context.coroutineScope.launch {
    186                             context.config.writeConfig()
    187                         }
    188                     }
    189                 }
    190             )
    191         }
    192 
    193         Column(
    194             modifier = Modifier
    195                 .fillMaxSize()
    196         ) {
    197             Text(
    198                 translation.format(
    199                     "spoofed_coordinates_title",
    200                     "latitude" to ((spoofedCoordinates?.first as? Double)?.toFloat() ?: "0.0").toString(),
    201                     "longitude" to ((spoofedCoordinates?.second as? Double)?.toFloat() ?: "0.0").toString()
    202                 ),
    203                 fontSize = 18.sp,
    204                 fontWeight = FontWeight.Bold,
    205                 textAlign = TextAlign.Center,
    206                 modifier = Modifier
    207                     .fillMaxWidth()
    208                     .padding(8.dp)
    209             )
    210 
    211             if (addSavedCoordinateDialog) {
    212                 me.rhunk.snapenhance.ui.util.Dialog(
    213                     onDismissRequest = { addSavedCoordinateDialog = false },
    214                     content = {
    215                         AddCoordinatesDialog(
    216                             alertDialogs,
    217                             translation,
    218                             LocationCoordinates().apply {
    219                                 this.latitude = marker.value?.position?.latitude ?: 0.0
    220                                 this.longitude = marker.value?.position?.longitude ?: 0.0
    221                             },
    222                         ) { coordinates ->
    223                             addSavedCoordinateDialog = false
    224                             addSavedCoordinate(null, coordinates) {
    225                                 withContext(Dispatchers.Main) {
    226                                     savedCoordinates.add(0, coordinates.apply { id = it })
    227                                 }
    228                             }
    229                         }
    230                     }
    231                 )
    232             }
    233 
    234             if (showMap) {
    235                 me.rhunk.snapenhance.ui.util.Dialog(
    236                     onDismissRequest = { showMap = false },
    237                     content = {
    238                         alertDialogs.ChooseLocationDialog(property = coordinatesProperty, marker, mapView, saveCoordinates = {
    239                             addSavedCoordinateDialog = true
    240                         }) {
    241                             showMap = false
    242                             context.config.writeConfig()
    243                         }
    244                         DisposableEffect(Unit) {
    245                             onDispose {
    246                                 marker.value = null
    247                             }
    248                         }
    249                     }
    250                 )
    251             }
    252 
    253             LazyColumn(
    254                 modifier = Modifier
    255                     .fillMaxSize()
    256                     .clipToBounds()
    257             ) {
    258 
    259                 item {
    260                     @Composable
    261                     fun ConfigToggle(
    262                         text: String,
    263                         state: MutableState<Boolean>,
    264                         onCheckedChange: (Boolean) -> Unit
    265                     ) {
    266                         Row(
    267                             modifier = Modifier.padding(start = 16.dp, end = 16.dp),
    268                             verticalAlignment = Alignment.CenterVertically
    269                         ) {
    270                             Text(text = text)
    271                             Spacer(modifier = Modifier.weight(1f))
    272                             Switch(
    273                                 checked = state.value,
    274                                 onCheckedChange = {
    275                                     state.value = it
    276                                     onCheckedChange(it)
    277                                 }
    278                             )
    279                         }
    280                     }
    281                     ConfigToggle(
    282                         translation["spoof_location_toggle"],
    283                         remember { mutableStateOf(context.config.root.global.betterLocation.spoofLocation.get()) }
    284                     ) {
    285                         context.config.root.global.betterLocation.spoofLocation.set(it)
    286                     }
    287                     ConfigToggle(
    288                         translation["suspend_location_updates"],
    289                         remember { mutableStateOf(context.config.root.global.betterLocation.suspendLocationUpdates.get()) }
    290                     ) {
    291                         context.config.root.global.betterLocation.suspendLocationUpdates.set(it)
    292                     }
    293                 }
    294                 item {
    295                     Row(
    296                         modifier = Modifier
    297                             .fillMaxWidth()
    298                             .padding(8.dp),
    299                         horizontalArrangement = Arrangement.SpaceEvenly,
    300                         verticalAlignment = Alignment.CenterVertically
    301                     ) {
    302                         Button(onClick = { showMap = true }) {
    303                             Text(translation["choose_location_button"])
    304                         }
    305                         Button(onClick = { showTeleportDialog = true }) {
    306                             Text(translation["teleport_to_friend_button"])
    307                         }
    308                     }
    309                 }
    310                 item {
    311                     Row(
    312                         modifier = Modifier
    313                             .fillMaxWidth()
    314                             .padding(start = 12.dp, end = 12.dp),
    315                         verticalAlignment = Alignment.CenterVertically,
    316                     ) {
    317                         Text(
    318                             translation["saved_coordinates_title"],
    319                             fontSize = 20.sp,
    320                             fontWeight = FontWeight.Bold,
    321                             modifier = Modifier.weight(1f),
    322                             lineHeight = 20.sp
    323                         )
    324                         IconButton(
    325                             onClick = {
    326                                 addSavedCoordinateDialog = true
    327                             }
    328                         ) {
    329                             Icon(Icons.Default.Add, contentDescription = "Add")
    330                         }
    331                     }
    332                 }
    333                 item {
    334                     if (savedCoordinates.isEmpty()) {
    335                         Text(
    336                             translation["no_saved_coordinates_hint"],
    337                             fontSize = 16.sp,
    338                             modifier = Modifier.padding(start = 20.dp),
    339                             fontWeight = FontWeight.Light
    340                         )
    341                     }
    342                 }
    343                 items(savedCoordinates, key = { it.id }) { coordinates ->
    344                     var mutableCoordinates by remember { mutableStateOf(coordinates) }
    345                     val isSelected = spoofedCoordinates == mutableCoordinates.latitude to mutableCoordinates.longitude
    346                     var showDeleteDialog by remember { mutableStateOf(false) }
    347                     var showEditDialog by remember { mutableStateOf(false) }
    348 
    349                     fun setSpoofedCoordinates() {
    350                         spoofedCoordinates = mutableCoordinates.latitude to mutableCoordinates.longitude
    351                         coordinatesProperty.value.setAny(spoofedCoordinates)
    352                         context.coroutineScope.launch {
    353                             context.config.writeConfig()
    354                         }
    355                     }
    356 
    357                     if (showDeleteDialog) {
    358                         me.rhunk.snapenhance.ui.util.Dialog(
    359                             onDismissRequest = { showDeleteDialog = false },
    360                             content = {
    361                                 alertDialogs.ConfirmDialog(
    362                                     title = translation["delete_dialog_title"],
    363                                     message = translation["delete_dialog_message"],
    364                                     onConfirm = {
    365                                         showDeleteDialog = false
    366                                         context.coroutineScope.launch {
    367                                             context.database.removeLocationCoordinate(coordinates.id)
    368                                             savedCoordinates.remove(coordinates)
    369                                         }
    370                                     },
    371                                     onDismiss = { showDeleteDialog = false }
    372                                 )
    373                             }
    374                         )
    375                     }
    376 
    377                     if (showEditDialog) {
    378                         me.rhunk.snapenhance.ui.util.Dialog(
    379                             onDismissRequest = { showEditDialog = false },
    380                             content = {
    381                                 AddCoordinatesDialog(
    382                                     alertDialogs,
    383                                     translation,
    384                                     mutableCoordinates
    385                                 ) {
    386                                     val itemId = coordinates.id
    387                                     context.coroutineScope.launch {
    388                                         addSavedCoordinate(itemId, it)
    389                                     }
    390                                     Parcel.obtain().apply {
    391                                         it.writeToParcel(this, 0)
    392                                         setDataPosition(0)
    393                                         coordinates.readFromParcel(this)
    394                                         coordinates.id = itemId
    395                                         recycle()
    396                                     }
    397                                     mutableCoordinates = it
    398                                     if (isSelected) setSpoofedCoordinates()
    399                                     showEditDialog = false
    400                                 }
    401                             }
    402                         )
    403                     }
    404 
    405                     ElevatedCard(
    406                         onClick = {
    407                             mutableCoordinates = coordinates
    408                             setSpoofedCoordinates()
    409                             GeoPoint(coordinates.latitude, coordinates.longitude).also {
    410                                 marker.value?.position = it
    411                                 mapView.value?.controller?.apply {
    412                                     animateTo(it)
    413                                     setZoom(16.0)
    414                                 }
    415                             }
    416                         },
    417                         modifier = Modifier
    418                             .fillMaxWidth()
    419                             .padding(5.dp),
    420                     ) {
    421                         Row(
    422                             modifier = Modifier
    423                                 .fillMaxWidth()
    424                                 .padding(4.dp),
    425                             verticalAlignment = Alignment.CenterVertically
    426                         ) {
    427                             Column(
    428                                 modifier = Modifier
    429                                     .padding(2.dp)
    430                                     .weight(1f)
    431                             ) {
    432                                 Text(
    433                                     text = remember(mutableCoordinates) { mutableCoordinates.name },
    434                                     fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Light,
    435                                     fontSize = 16.sp,
    436                                     lineHeight = 20.sp,
    437                                     overflow = TextOverflow.Ellipsis
    438                                 )
    439                                 Text(
    440                                     text = remember(mutableCoordinates) { "(${mutableCoordinates.latitude.toFloat()}, ${mutableCoordinates.longitude.toFloat()})" },
    441                                     fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Light,
    442                                     fontSize = 12.sp,
    443                                     lineHeight = 15.sp,
    444                                     overflow = TextOverflow.Ellipsis
    445                                 )
    446                             }
    447                             FilledIconButton(onClick = {
    448                                 showEditDialog = true
    449                             }) {
    450                                 Icon(Icons.Default.Edit, contentDescription = "Delete")
    451                             }
    452                             Spacer(modifier = Modifier.width(4.dp))
    453                             FilledIconButton(onClick = {
    454                                 showDeleteDialog = true
    455                             }) {
    456                                 Icon(Icons.Default.DeleteOutline, contentDescription = "Delete")
    457                             }
    458                         }
    459                     }
    460                 }
    461             }
    462         }
    463     }
    464 }