Advertisement
Guido_Fe

Untitled

Dec 14th, 2022
1,192
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Kotlin 18.32 KB | None | 0 0
  1. package com.guidofe.pocketlibrary.ui.pages.editbook
  2.  
  3. import android.Manifest
  4. import android.content.ActivityNotFoundException
  5. import android.net.Uri
  6. import android.os.Build
  7. import android.util.Log
  8. import androidx.activity.compose.rememberLauncherForActivityResult
  9. import androidx.activity.result.PickVisualMediaRequest
  10. import androidx.activity.result.contract.ActivityResultContracts
  11. import androidx.compose.foundation.clickable
  12. import androidx.compose.foundation.layout.*
  13. import androidx.compose.foundation.lazy.LazyRow
  14. import androidx.compose.foundation.lazy.items
  15. import androidx.compose.foundation.rememberScrollState
  16. import androidx.compose.foundation.text.KeyboardOptions
  17. import androidx.compose.foundation.verticalScroll
  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.input.nestedscroll.nestedScroll
  23. import androidx.compose.ui.platform.LocalContext
  24. import androidx.compose.ui.platform.LocalFocusManager
  25. import androidx.compose.ui.platform.LocalLifecycleOwner
  26. import androidx.compose.ui.res.painterResource
  27. import androidx.compose.ui.res.stringResource
  28. import androidx.compose.ui.text.input.KeyboardType
  29. import androidx.compose.ui.tooling.preview.Preview
  30. import androidx.compose.ui.unit.dp
  31. import androidx.hilt.navigation.compose.hiltViewModel
  32. import androidx.lifecycle.Lifecycle
  33. import androidx.lifecycle.LifecycleEventObserver
  34. import coil.compose.AsyncImage
  35. import coil.request.ImageRequest
  36. import com.google.accompanist.permissions.ExperimentalPermissionsApi
  37. import com.google.accompanist.permissions.isGranted
  38. import com.google.accompanist.permissions.rememberPermissionState
  39. import com.google.accompanist.permissions.shouldShowRationale
  40. import com.guidofe.pocketlibrary.R
  41. import com.guidofe.pocketlibrary.ui.modules.*
  42. import com.guidofe.pocketlibrary.ui.pages.destinations.TakeCoverPhotoPageDestination
  43. import com.guidofe.pocketlibrary.utils.BookDestination
  44. import com.guidofe.pocketlibrary.utils.isPermanentlyDenied
  45. import com.guidofe.pocketlibrary.viewmodels.EditBookVM
  46. import com.guidofe.pocketlibrary.viewmodels.interfaces.IEditBookVM
  47. import com.guidofe.pocketlibrary.viewmodels.previews.EditBookVMPreview
  48. import com.ramcosta.composedestinations.annotation.Destination
  49. import com.ramcosta.composedestinations.navigation.DestinationsNavigator
  50. import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
  51. import com.ramcosta.composedestinations.result.EmptyResultRecipient
  52. import com.ramcosta.composedestinations.result.NavResult
  53. import com.ramcosta.composedestinations.result.ResultRecipient
  54. import java.io.File
  55. import kotlinx.coroutines.Dispatchers
  56. import kotlinx.coroutines.launch
  57. import kotlinx.coroutines.withContext
  58.  
  59. val verticalSpace = 8.dp
  60. val horizontalSpace = 8.dp
  61. @OptIn(
  62.     ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class
  63. )
  64. @Destination
  65. @Composable
  66. fun EditBookPage(
  67.     bookId: Long? = null,
  68.     isbn: String? = null,
  69.     newBookDestination: BookDestination? = null,
  70.     navigator: DestinationsNavigator,
  71.     vm: IEditBookVM = hiltViewModel<EditBookVM>(),
  72.     coverPhotoRecipient: ResultRecipient<TakeCoverPhotoPageDestination, Uri>
  73. ) {
  74.     coverPhotoRecipient.onNavResult { result ->
  75.         Log.d("debug", "EditPage received result")
  76.         if (result is NavResult.Value) {
  77.             Log.d("debug", "EditPage result is valid, == ${result.value}")
  78.             vm.state.coverUri = result.value
  79.         } else {
  80.             Log.e("debug", "EditPage result is not valid")
  81.         }
  82.     }
  83.     val scrollState = rememberScrollState()
  84.     val coroutineScope = rememberCoroutineScope()
  85.     val context = LocalContext.current
  86.     var imageRequest: ImageRequest? by remember { mutableStateOf(null) }
  87.     var showRationaleDialog by remember { mutableStateOf(false) }
  88.     var showPermissionDeniedDialog by remember { mutableStateOf(false) }
  89.     val lifecycleOwner = LocalLifecycleOwner.current
  90.     val focusManager = LocalFocusManager.current
  91.     DisposableEffect(lifecycleOwner) {
  92.         val observer = LifecycleEventObserver { _, event ->
  93.             when (event) {
  94.                 Lifecycle.Event.ON_CREATE -> {
  95.                     coroutineScope.launch(Dispatchers.IO) {
  96.                         bookId?.let { id ->
  97.                             val previousUri = vm.state.coverUri
  98.                             vm.initialiseFromDatabase(id)
  99.                             previousUri?.let { vm.state.coverUri = it }
  100.                         }
  101.                         isbn?.let {
  102.                             Log.d("debug", "Setting isbn $it")
  103.                             vm.state.identifier = it
  104.                         }
  105.                     }
  106.                 }
  107.                 Lifecycle.Event.ON_DESTROY -> {
  108.                     File(vm.getTempCoverUri().path!!).delete()
  109.                 }
  110.                 else -> {}
  111.             }
  112.         }
  113.         lifecycleOwner.lifecycle.addObserver(observer)
  114.  
  115.         onDispose {
  116.             lifecycleOwner.lifecycle.removeObserver(observer)
  117.         }
  118.     }
  119.  
  120.     vm.scaffoldState.scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
  121.  
  122.     val uploadFileLauncher = rememberLauncherForActivityResult(
  123.         ActivityResultContracts.PickVisualMedia(),
  124.         onResult = { result ->
  125.             val tempUri = vm.getTempCoverUri()
  126.             result?.let { uri ->
  127.                 val resolver = context.contentResolver
  128.                 resolver.openInputStream(uri).use { iStr ->
  129.                     File(tempUri.path!!).outputStream().use { oStr ->
  130.                         iStr?.copyTo(oStr)
  131.                     }
  132.                 }
  133.                 vm.state.coverUri = tempUri
  134.             }
  135.         }
  136.     )
  137.     val permissionState = rememberPermissionState(
  138.         permission =
  139.         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
  140.             Manifest.permission.READ_MEDIA_IMAGES
  141.         else Manifest.permission.READ_EXTERNAL_STORAGE,
  142.         onPermissionResult = {
  143.             if (it) {
  144.                 uploadFileLauncher.launch(
  145.                     PickVisualMediaRequest(
  146.                         ActivityResultContracts.PickVisualMedia.ImageOnly
  147.                     )
  148.                 )
  149.             }
  150.         }
  151.     )
  152.     LaunchedEffect(vm.state.coverUri) {
  153.         imageRequest = vm.state.coverUri?.let { uri ->
  154.             ImageRequest.Builder(context)
  155.                 .data(uri)
  156.                 .build()
  157.         }
  158.     }
  159.     LaunchedEffect(key1 = true) {
  160.         vm.scaffoldState.refreshBar(
  161.             title = { Text(stringResource(R.string.edit_book)) },
  162.             actions = {
  163.                 IconButton(
  164.                     onClick = {
  165.                         coroutineScope.launch(Dispatchers.IO) {
  166.                             val id = vm.submitBook(newBookDestination)
  167.                             if (id <= 0L) {
  168.                                 vm.snackbarHostState.showSnackbar(
  169.                                     CustomSnackbarVisuals(
  170.                                         context.getString(R.string.error_cant_save_book),
  171.                                         true
  172.                                     )
  173.                                 )
  174.                             } else {
  175.                                 withContext(Dispatchers.Main) {
  176.                                     navigator.navigateUp()
  177.                                 }
  178.                             }
  179.                         }
  180.                     }
  181.                 ) {
  182.                     Icon(
  183.                         painterResource(id = R.drawable.check_24px),
  184.                         stringResource(R.string.save)
  185.                     )
  186.                 }
  187.             },
  188.             navigationIcon = {
  189.                 IconButton(onClick = {
  190.                     File(vm.getTempCoverUri().path!!).delete()
  191.                     navigator.navigateUp()
  192.                 }) {
  193.                     Icon(
  194.                         painterResource(R.drawable.arrow_back_24px),
  195.                         stringResource(R.string.back)
  196.                     )
  197.                 }
  198.             }
  199.         )
  200.     }
  201.     Column(
  202.         horizontalAlignment = Alignment.CenterHorizontally,
  203.         verticalArrangement = Arrangement.spacedBy(verticalSpace),
  204.         modifier = Modifier
  205.             .verticalScroll(scrollState)
  206.             .fillMaxWidth()
  207.             .nestedScroll(vm.scaffoldState.scrollBehavior!!.nestedScrollConnection)
  208.             .padding(8.dp)
  209.     ) {
  210.         BoxWithConstraints {
  211.             Box(
  212.                 modifier = Modifier
  213.                     .clickable { vm.state.showCoverMenu = true }
  214.             ) {
  215.                 if (imageRequest != null) {
  216.                     // TODO: placeholder for book cover
  217.                     AsyncImage(
  218.                         model = imageRequest,
  219.                         contentDescription = stringResource(id = R.string.cover),
  220.                         Modifier.height(200.dp)
  221.                     )
  222.                 } else
  223.                     EmptyBookCover(Modifier.width(90.dp))
  224.             }
  225.         }
  226.         OutlinedTextField(
  227.             value = vm.state.title,
  228.             label = { Text(stringResource(id = R.string.title) + "*") },
  229.             onValueChange = { vm.state.title = it },
  230.             singleLine = true,
  231.             modifier = Modifier.fillMaxWidth()
  232.         )
  233.         OutlinedTextField(
  234.             value = vm.state.subtitle,
  235.             label = { Text(stringResource(id = R.string.subtitle)) },
  236.             singleLine = true,
  237.             onValueChange = { vm.state.subtitle = it },
  238.             modifier = Modifier.fillMaxWidth()
  239.         )
  240.         OutlinedTextField(
  241.             value = vm.state.authors,
  242.             onValueChange = { vm.state.authors = it },
  243.             singleLine = true,
  244.             label = { Text(stringResource(R.string.authors)) },
  245.             modifier = Modifier.fillMaxWidth()
  246.         )
  247.         LazyRow(
  248.             horizontalArrangement = Arrangement.spacedBy(8.dp),
  249.             modifier = Modifier.fillMaxWidth()
  250.         ) {
  251.             items(items = vm.state.genres) { genre ->
  252.                 InputChip(
  253.                     selected = true,
  254.                     onClick = {},
  255.                     label = { Text(genre) },
  256.                     trailingIcon = {
  257.                         IconButton(
  258.                             onClick = {
  259.                                 vm.state.genres -= genre
  260.                             },
  261.                             modifier = Modifier.size(InputChipDefaults.IconSize)
  262.                         ) {
  263.                             Icon(
  264.                                 painter = painterResource(R.drawable.close_24px),
  265.                                 contentDescription = stringResource(R.string.delete)
  266.                             )
  267.                         }
  268.                     }
  269.                 )
  270.             }
  271.         }
  272.         Row(
  273.             verticalAlignment = Alignment.CenterVertically
  274.         ) {
  275.             OutlinedAutocomplete(
  276.                 text = vm.state.genreInput,
  277.                 onTextChange = {
  278.                     vm.state.genreInput = it
  279.                     if (it.length == 3)
  280.                         vm.updateExistingGenres(it)
  281.                 },
  282.                 options = vm.state.existingGenres,
  283.                 label = { Text(stringResource(R.string.new_genre)) },
  284.                 onOptionSelected = { vm.state.genreInput = it },
  285.                 modifier = Modifier.weight(1f)
  286.             )
  287.             IconButton(
  288.                 onClick = {
  289.                     if (vm.state.genreInput.isBlank())
  290.                         return@IconButton
  291.                     vm.state.genres += vm.state.genreInput
  292.                     vm.state.genreInput = ""
  293.                 }
  294.             ) {
  295.                 Icon(painterResource(R.drawable.add_24px), stringResource(R.string.add))
  296.             }
  297.         }
  298.         OutlinedTextField(
  299.             value = vm.state.description,
  300.             onValueChange = { vm.state.description = it },
  301.             label = { Text(stringResource(id = R.string.summary)) },
  302.             modifier = Modifier.fillMaxWidth()
  303.         )
  304.         Row(
  305.             horizontalArrangement = Arrangement.spacedBy(horizontalSpace),
  306.             verticalAlignment = Alignment.CenterVertically
  307.         ) {
  308.             LanguageAutocomplete(
  309.                 text = vm.state.language,
  310.                 onTextChange = { vm.state.language = it },
  311.                 onOptionSelected = { vm.state.language = it },
  312.                 label = { Text(stringResource(R.string.language)) },
  313.                 isError = vm.state.isLanguageError,
  314.                 modifier = Modifier
  315.                     .weight(1f)
  316.             )
  317.             Column(
  318.                 // verticalArrangement = Arrangement.spacedBy(2.dp),
  319.                 horizontalAlignment = Alignment.CenterHorizontally
  320.             ) {
  321.                 Text(stringResource(R.string.is_ebook))
  322.                 Switch(
  323.                     checked = vm.state.isEbook,
  324.                     onCheckedChange = {
  325.                         vm.state.isEbook = !vm.state.isEbook
  326.                     },
  327.                 )
  328.             }
  329.         }
  330.         Row(
  331.             horizontalArrangement = Arrangement.spacedBy(horizontalSpace)
  332.         ) {
  333.             OutlinedTextField(
  334.                 value = vm.state.publisher,
  335.                 onValueChange = { vm.state.publisher = it },
  336.                 label = { Text(stringResource(R.string.publisher)) },
  337.                 singleLine = true,
  338.                 modifier = Modifier
  339.                     .weight(2f)
  340.             )
  341.             OutlinedTextField(
  342.                 value = vm.state.published,
  343.                 onValueChange = {
  344.                     vm.state.published = it
  345.                 },
  346.                 label = { Text(stringResource(R.string.year)) },
  347.                 singleLine = true,
  348.                 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
  349.                 modifier = Modifier
  350.                     .weight(1f)
  351.             )
  352.         }
  353.         OutlinedTextField(
  354.             value = vm.state.identifier,
  355.             onValueChange = { vm.state.identifier = it },
  356.             singleLine = true,
  357.             keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
  358.             label = { Text(stringResource(R.string.isbn)) },
  359.             modifier = Modifier.fillMaxWidth()
  360.         )
  361.     }
  362.     ModalBottomSheet(
  363.         visible = vm.state.showCoverMenu,
  364.         onDismiss = { vm.state.showCoverMenu = false }
  365.     ) {
  366.         RowWithIcon(
  367.             icon = {
  368.                 Icon(
  369.                     painterResource(R.drawable.photo_camera_24px),
  370.                     stringResource(R.string.camera)
  371.                 )
  372.             },
  373.             onClick = {
  374.                 try {
  375.                     val uri = vm.getTempCoverUri()
  376.                     navigator.navigate(TakeCoverPhotoPageDestination(uri))
  377.                 } catch (e: ActivityNotFoundException) {
  378.                     coroutineScope.launch {
  379.                         vm.snackbarHostState.showSnackbar(
  380.                             CustomSnackbarVisuals(
  381.                                 message = context.getString(R.string.error_no_camera),
  382.                                 isError = true
  383.                             )
  384.                         )
  385.                     }
  386.                 }
  387.                 vm.state.showCoverMenu = false
  388.             }
  389.         ) {
  390.             Text(stringResource(R.string.take_photo))
  391.         }
  392.         RowWithIcon(
  393.             icon = {
  394.                 Icon(painterResource(R.drawable.upload_24px), stringResource(R.string.upload))
  395.             },
  396.             onClick = {
  397.                 when {
  398.                     permissionState.status.isGranted -> {
  399.                         Log.d("debug", "Permission granted")
  400.                         permissionState.launchPermissionRequest()
  401.                     }
  402.                     permissionState.status.shouldShowRationale -> {
  403.                         showRationaleDialog = true
  404.                     }
  405.                     permissionState.status.isPermanentlyDenied -> {
  406.                         showPermissionDeniedDialog = true
  407.                     }
  408.                 }
  409.                 vm.state.showCoverMenu = false
  410.             }
  411.         ) {
  412.             Text(stringResource(R.string.choose_from_gallery))
  413.         }
  414.         RowWithIcon(
  415.             icon = {
  416.                 Icon(
  417.                     painterResource(
  418.                         R.drawable.delete_24px
  419.                     ),
  420.                     stringResource(R.string.clear_cover)
  421.                 )
  422.             },
  423.             onClick = {
  424.                 vm.state.coverUri = null
  425.                 vm.state.showCoverMenu = false
  426.             }
  427.         ) {
  428.             Text(stringResource(R.string.clear_cover))
  429.         }
  430.     }
  431.     if (showRationaleDialog) {
  432.         AlertDialog(
  433.             onDismissRequest = { showRationaleDialog = false },
  434.             confirmButton = {
  435.                 Button(onClick = { showRationaleDialog = false }) {
  436.                     Text(stringResource(R.string.deny))
  437.                 }
  438.             },
  439.             title = { Text(stringResource(R.string.permission_required)) },
  440.             dismissButton = {
  441.                 OutlinedButton(
  442.                     onClick = {
  443.                         permissionState.launchPermissionRequest()
  444.                         showRationaleDialog = false
  445.                     }
  446.                 ) {
  447.                     Text(stringResource(R.string.ask_again))
  448.                 }
  449.             },
  450.             text = { Text(stringResource(R.string.gallery_rationale)) }
  451.         )
  452.     }
  453.  
  454.     if (showPermissionDeniedDialog) {
  455.         AlertDialog(
  456.             onDismissRequest = { showPermissionDeniedDialog = false },
  457.             confirmButton = {
  458.                 Button(onClick = { showPermissionDeniedDialog = false }) {
  459.                     Text(stringResource(R.string.ok_label))
  460.                 }
  461.             },
  462.             text = { Text(stringResource(R.string.gallery_denied)) }
  463.         )
  464.     }
  465. }
  466.  
  467. @Composable
  468. @Preview(showSystemUi = true)
  469. private fun ImportedBookFormPagePreview() {
  470.     EditBookPage(
  471.         bookId = 0,
  472.         navigator = EmptyDestinationsNavigator,
  473.         vm = EditBookVMPreview(),
  474.         coverPhotoRecipient = EmptyResultRecipient()
  475.     )
  476. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement