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 }