123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- 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<Uri>) -> 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<Uri> {
- 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<AttachmentItem?>
- ) : Parcelable
- val AttachmentItemListType = object : NavType<AttachmentItemList>(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<AttachmentItemList>(key)
- }
- override fun parseValue(value: String): AttachmentItemList {
- return Json.decodeFromString<AttachmentItemList>(value)
- }
- }
- fun AttachmentItemList.toArrayListUri(): ArrayList<Uri> {
- val list = arrayListOf<Uri>()
- this.list.forEach {
- it?.let {
- list.add(it.uri)
- }
- }
- return list
- }
|