package io.nexilis.alpha.ui.components import android.Manifest import android.content.Context import android.net.Uri import android.os.Bundle import android.os.Parcelable import android.provider.OpenableColumns import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.Camera import androidx.compose.material.icons.filled.Headset import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.PhotoCamera import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TooltipDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.core.content.FileProvider import androidx.core.net.toUri import androidx.navigation.NavType import io.nexilis.alpha.BuildConfig import io.nexilis.service.core.BlueSky import io.nexilis.service.core.DarkLimeGreen import io.nexilis.service.core.Orange import io.nexilis.service.core.Purple import io.nexilis.service.core.createFile import io.nexilis.service.core.getMimeType import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json @OptIn(ExperimentalMaterial3Api::class) @Composable fun Attachments(modifier: Modifier, onAttachment: (List) -> Unit) { val keyboardController = LocalSoftwareKeyboardController.current val positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider() var isOpenMenu by remember { mutableStateOf(false) } var isDismiss by remember { mutableStateOf(false) } val context = LocalContext.current var tempUri = Uri.EMPTY val coroutine = rememberCoroutineScope() IconButton(onClick = { if (isDismiss) { return@IconButton } keyboardController?.hide() isOpenMenu = true } ) { Icon( imageVector = Icons.Default.Add, contentDescription = "", tint = MaterialTheme.colorScheme.primary ) } if (isOpenMenu) { Popup(popupPositionProvider = positionProvider, onDismissRequest = { isDismiss = true coroutine.launch { delay(100) isDismiss = false } isOpenMenu = false }) { Box(modifier = modifier) { Row(Modifier.align(Alignment.Center)) { var mimeType = arrayOf( "text/*", "application/*" ) val launcherFiles = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { onAttachment(it) isOpenMenu = false } val permissionLauncherFiles = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { if (it) { launcherFiles.launch(mimeType) } else { Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT) .show() } } val launcherCamera = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { if (it) { onAttachment(listOf(tempUri)) } isOpenMenu = false } val permissionLauncherCamera = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { if (it) { val file = context.createFile(".jpeg") tempUri = FileProvider.getUriForFile( context, BuildConfig.APPLICATION_ID + ".provider", file ) launcherCamera.launch(tempUri) } else { Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT) .show() } } val launcherCaptureVideo = rememberLauncherForActivityResult(ActivityResultContracts.CaptureVideo()) { if (it) { onAttachment(listOf(tempUri)) } isOpenMenu = false } val permissionLauncherCaptureVideo = rememberLauncherForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { var accept = true it.values.forEach { r -> if (!r) accept = false } if (accept) { val file = context.createFile(".mp4") tempUri = FileProvider.getUriForFile( context, BuildConfig.APPLICATION_ID + ".provider", file ) launcherCaptureVideo.launch(tempUri) } else { Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT) .show() } } Column { AttachmentButton( color = MaterialTheme.colorScheme.primary, onClick = { permissionLauncherFiles.launch(Manifest.permission.READ_EXTERNAL_STORAGE) }, text = { Text( text = "File", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodySmall ) }) { Icon( imageVector = Icons.Default.AttachFile, contentDescription = "", tint = Color.White ) } Spacer(modifier = Modifier.size(32.dp)) AttachmentButton(color = Color.Purple, onClick = { mimeType = arrayOf( "image/*", "video/*" ) permissionLauncherFiles.launch(Manifest.permission.READ_EXTERNAL_STORAGE) }, text = { Text( text = "Gallery", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodySmall ) }) { Icon( imageVector = Icons.Default.Image, contentDescription = "", tint = Color.White ) } } Spacer(modifier = Modifier.size(16.dp)) Column { AttachmentButton(color = Color.Red, onClick = { permissionLauncherCamera.launch(Manifest.permission.CAMERA) }, text = { Text( text = "Photo", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodySmall ) }) { Icon( imageVector = Icons.Default.PhotoCamera, contentDescription = "", tint = Color.White ) } Spacer(modifier = Modifier.size(32.dp)) AttachmentButton(color = Color.Orange, onClick = { mimeType = arrayOf( "audio/*" ) permissionLauncherFiles.launch(Manifest.permission.READ_EXTERNAL_STORAGE) }, text = { Text( text = "Audio", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodySmall ) }) { Icon( imageVector = Icons.Default.Headset, contentDescription = "", tint = Color.White ) } } Spacer(modifier = Modifier.size(16.dp)) Column { AttachmentButton(color = Color.DarkLimeGreen, onClick = { permissionLauncherCaptureVideo.launch( arrayOf( Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO ) ) }, text = { Text( text = "Video", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodySmall ) }) { Icon( imageVector = Icons.Default.Camera, contentDescription = "", tint = Color.White ) } Spacer(modifier = Modifier.size(32.dp)) AttachmentButton(color = Color.BlueSky, onClick = {}, text = { Text( text = "Location", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodySmall ) }) { Icon( imageVector = Icons.Default.LocationOn, contentDescription = "", tint = Color.White ) } } } } } } } @Composable fun AttachmentButton( color: Color = MaterialTheme.colorScheme.primary, onClick: () -> Unit, text: @Composable () -> Unit, content: @Composable () -> Unit ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( modifier = Modifier .then(Modifier.size(72.dp)) .graphicsLayer { shape = CircleShape clip = true } .clickable(onClick = onClick) .padding(0.dp) .background(color), contentAlignment = Alignment.Center ) { content() } Spacer(modifier = Modifier.size(6.dp)) text() } } fun Context.toAttachmentItem(uri: Uri): AttachmentItem? { val item = contentResolver.query(uri, null, null, null, null) ?.use { cursor -> val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) cursor.moveToFirst() AttachmentItem( cursor.getString(nameIndex), cursor.getLong(sizeIndex), getMimeType(uri), uri ) } return item } object UriSerializable : KSerializer { override val descriptor: SerialDescriptor get() = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): Uri { return decoder.decodeString().toUri() } override fun serialize(encoder: Encoder, value: Uri) { encoder.encodeString(value.toString()) } } @Serializable @Parcelize data class AttachmentItem( val name: String, val size: Long, val mimeType: String, @Serializable(with = UriSerializable::class) val uri: Uri ) : Parcelable @Serializable @Parcelize data class AttachmentItemList( val list: List ) : Parcelable val AttachmentItemListType = object : NavType(isNullableAllowed = false) { override fun put(bundle: Bundle, key: String, value: AttachmentItemList) { bundle.putParcelable(key, value) } override fun get(bundle: Bundle, key: String): AttachmentItemList? { return bundle.getParcelable(key) } override fun parseValue(value: String): AttachmentItemList { return Json.decodeFromString(value) } } fun AttachmentItemList.toArrayListUri(): ArrayList { val list = arrayListOf() this.list.forEach { it?.let { list.add(it.uri) } } return list }