Attachments.kt 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. package io.nexilis.alpha.ui.components
  2. import android.Manifest
  3. import android.content.Context
  4. import android.net.Uri
  5. import android.os.Bundle
  6. import android.os.Parcelable
  7. import android.provider.OpenableColumns
  8. import android.widget.Toast
  9. import androidx.activity.compose.rememberLauncherForActivityResult
  10. import androidx.activity.result.contract.ActivityResultContracts
  11. import androidx.compose.foundation.background
  12. import androidx.compose.foundation.clickable
  13. import androidx.compose.foundation.layout.Box
  14. import androidx.compose.foundation.layout.Column
  15. import androidx.compose.foundation.layout.Row
  16. import androidx.compose.foundation.layout.Spacer
  17. import androidx.compose.foundation.layout.padding
  18. import androidx.compose.foundation.layout.size
  19. import androidx.compose.foundation.shape.CircleShape
  20. import androidx.compose.material.icons.Icons
  21. import androidx.compose.material.icons.filled.Add
  22. import androidx.compose.material.icons.filled.AttachFile
  23. import androidx.compose.material.icons.filled.Camera
  24. import androidx.compose.material.icons.filled.Headset
  25. import androidx.compose.material.icons.filled.Image
  26. import androidx.compose.material.icons.filled.LocationOn
  27. import androidx.compose.material.icons.filled.PhotoCamera
  28. import androidx.compose.material3.ExperimentalMaterial3Api
  29. import androidx.compose.material3.Icon
  30. import androidx.compose.material3.IconButton
  31. import androidx.compose.material3.MaterialTheme
  32. import androidx.compose.material3.Text
  33. import androidx.compose.material3.TooltipDefaults
  34. import androidx.compose.runtime.Composable
  35. import androidx.compose.runtime.getValue
  36. import androidx.compose.runtime.mutableStateOf
  37. import androidx.compose.runtime.remember
  38. import androidx.compose.runtime.setValue
  39. import androidx.compose.ui.Alignment
  40. import androidx.compose.ui.Modifier
  41. import androidx.compose.ui.graphics.Color
  42. import androidx.compose.ui.graphics.graphicsLayer
  43. import androidx.compose.ui.platform.LocalContext
  44. import androidx.compose.ui.platform.LocalSoftwareKeyboardController
  45. import androidx.compose.ui.unit.dp
  46. import androidx.compose.ui.window.Popup
  47. import androidx.core.content.FileProvider
  48. import androidx.core.net.toUri
  49. import androidx.navigation.NavType
  50. import io.nexilis.alpha.BuildConfig
  51. import io.nexilis.service.core.BlueSky
  52. import io.nexilis.service.core.DarkLimeGreen
  53. import io.nexilis.service.core.Orange
  54. import io.nexilis.service.core.Purple
  55. import io.nexilis.service.core.createFile
  56. import io.nexilis.service.core.getMimeType
  57. import kotlinx.parcelize.Parcelize
  58. import kotlinx.serialization.KSerializer
  59. import kotlinx.serialization.Serializable
  60. import kotlinx.serialization.descriptors.PrimitiveKind
  61. import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
  62. import kotlinx.serialization.descriptors.SerialDescriptor
  63. import kotlinx.serialization.encoding.Decoder
  64. import kotlinx.serialization.encoding.Encoder
  65. import kotlinx.serialization.json.Json
  66. @OptIn(ExperimentalMaterial3Api::class)
  67. @Composable
  68. fun Attachments(modifier: Modifier, onAttachment: (List<Uri>) -> Unit) {
  69. val keyboardController = LocalSoftwareKeyboardController.current
  70. val positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider()
  71. var isOpenMenu by remember {
  72. mutableStateOf(false)
  73. }
  74. val context = LocalContext.current
  75. var tempUri = Uri.EMPTY
  76. IconButton(onClick = {
  77. keyboardController?.hide()
  78. isOpenMenu = !isOpenMenu
  79. }
  80. ) {
  81. Icon(
  82. imageVector = Icons.Default.Add,
  83. contentDescription = "",
  84. tint = MaterialTheme.colorScheme.primary
  85. )
  86. }
  87. if (isOpenMenu) {
  88. Popup(popupPositionProvider = positionProvider, onDismissRequest = { isOpenMenu = false }) {
  89. Box(modifier = modifier) {
  90. Row(Modifier.align(Alignment.Center)) {
  91. var mimeType = arrayOf(
  92. "doc/*",
  93. "docx/*",
  94. "application/*"
  95. )
  96. val launcherFiles =
  97. rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) {
  98. onAttachment(it)
  99. isOpenMenu = false
  100. }
  101. val permissionLauncherFiles = rememberLauncherForActivityResult(
  102. ActivityResultContracts.RequestPermission()
  103. ) {
  104. if (it) {
  105. launcherFiles.launch(mimeType)
  106. } else {
  107. Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT)
  108. .show()
  109. }
  110. }
  111. val launcherCamera =
  112. rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
  113. if (it) {
  114. onAttachment(listOf(tempUri))
  115. }
  116. isOpenMenu = false
  117. }
  118. val permissionLauncherCamera = rememberLauncherForActivityResult(
  119. ActivityResultContracts.RequestPermission()
  120. ) {
  121. if (it) {
  122. val file = context.createFile(".jpeg")
  123. tempUri = FileProvider.getUriForFile(
  124. context,
  125. BuildConfig.APPLICATION_ID + ".provider", file
  126. )
  127. launcherCamera.launch(tempUri)
  128. } else {
  129. Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT)
  130. .show()
  131. }
  132. }
  133. val launcherCaptureVideo =
  134. rememberLauncherForActivityResult(ActivityResultContracts.CaptureVideo()) {
  135. if (it) {
  136. onAttachment(listOf(tempUri))
  137. }
  138. isOpenMenu = false
  139. }
  140. val permissionLauncherCaptureVideo = rememberLauncherForActivityResult(
  141. ActivityResultContracts.RequestMultiplePermissions()
  142. ) {
  143. var accept = true
  144. it.values.forEach { r ->
  145. if (!r) accept = false
  146. }
  147. if (accept) {
  148. val file = context.createFile(".mp4")
  149. tempUri = FileProvider.getUriForFile(
  150. context,
  151. BuildConfig.APPLICATION_ID + ".provider", file
  152. )
  153. launcherCaptureVideo.launch(tempUri)
  154. } else {
  155. Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT)
  156. .show()
  157. }
  158. }
  159. Column {
  160. AttachmentButton(
  161. color = MaterialTheme.colorScheme.primary,
  162. onClick = {
  163. permissionLauncherFiles.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
  164. },
  165. text = {
  166. Text(text = "File", style = MaterialTheme.typography.bodySmall)
  167. }) {
  168. Icon(
  169. imageVector = Icons.Default.AttachFile,
  170. contentDescription = "",
  171. tint = Color.White
  172. )
  173. }
  174. Spacer(modifier = Modifier.size(32.dp))
  175. AttachmentButton(color = Color.Purple, onClick = {
  176. mimeType = arrayOf(
  177. "image/*",
  178. "video/*"
  179. )
  180. permissionLauncherFiles.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
  181. }, text = {
  182. Text(text = "Gallery", style = MaterialTheme.typography.bodySmall)
  183. }) {
  184. Icon(
  185. imageVector = Icons.Default.Image,
  186. contentDescription = "",
  187. tint = Color.White
  188. )
  189. }
  190. }
  191. Spacer(modifier = Modifier.size(16.dp))
  192. Column {
  193. AttachmentButton(color = Color.Red, onClick = {
  194. permissionLauncherCamera.launch(Manifest.permission.CAMERA)
  195. }, text = {
  196. Text(text = "Photo", style = MaterialTheme.typography.bodySmall)
  197. }) {
  198. Icon(
  199. imageVector = Icons.Default.PhotoCamera,
  200. contentDescription = "",
  201. tint = Color.White
  202. )
  203. }
  204. Spacer(modifier = Modifier.size(32.dp))
  205. AttachmentButton(color = Color.Orange, onClick = {
  206. mimeType = arrayOf(
  207. "audio/*"
  208. )
  209. permissionLauncherFiles.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
  210. }, text = {
  211. Text(text = "Audio", style = MaterialTheme.typography.bodySmall)
  212. }) {
  213. Icon(
  214. imageVector = Icons.Default.Headset,
  215. contentDescription = "",
  216. tint = Color.White
  217. )
  218. }
  219. }
  220. Spacer(modifier = Modifier.size(16.dp))
  221. Column {
  222. AttachmentButton(color = Color.DarkLimeGreen, onClick = {
  223. permissionLauncherCaptureVideo.launch(
  224. arrayOf(
  225. Manifest.permission.CAMERA,
  226. Manifest.permission.RECORD_AUDIO
  227. )
  228. )
  229. }, text = {
  230. Text(text = "Video", style = MaterialTheme.typography.bodySmall)
  231. }) {
  232. Icon(
  233. imageVector = Icons.Default.Camera,
  234. contentDescription = "",
  235. tint = Color.White
  236. )
  237. }
  238. Spacer(modifier = Modifier.size(32.dp))
  239. AttachmentButton(color = Color.BlueSky, onClick = {}, text = {
  240. Text(text = "Location", style = MaterialTheme.typography.bodySmall)
  241. }) {
  242. Icon(
  243. imageVector = Icons.Default.LocationOn,
  244. contentDescription = "",
  245. tint = Color.White
  246. )
  247. }
  248. }
  249. }
  250. }
  251. }
  252. }
  253. }
  254. @Composable
  255. fun AttachmentButton(
  256. color: Color = MaterialTheme.colorScheme.primary,
  257. onClick: () -> Unit,
  258. text: @Composable () -> Unit,
  259. content: @Composable () -> Unit
  260. ) {
  261. Column(horizontalAlignment = Alignment.CenterHorizontally) {
  262. Box(
  263. modifier = Modifier
  264. .then(Modifier.size(72.dp))
  265. .graphicsLayer {
  266. shape = CircleShape
  267. clip = true
  268. }
  269. .clickable(onClick = onClick)
  270. .padding(0.dp)
  271. .background(color),
  272. contentAlignment = Alignment.Center
  273. ) {
  274. content()
  275. }
  276. Spacer(modifier = Modifier.size(6.dp))
  277. text()
  278. }
  279. }
  280. fun Context.toAttachmentItem(uri: Uri): AttachmentItem? {
  281. val item = contentResolver.query(uri, null, null, null, null)
  282. ?.use { cursor ->
  283. val nameIndex =
  284. cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
  285. val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
  286. cursor.moveToFirst()
  287. AttachmentItem(
  288. cursor.getString(nameIndex),
  289. cursor.getLong(sizeIndex),
  290. getMimeType(uri),
  291. uri
  292. )
  293. }
  294. return item
  295. }
  296. object UriSerializable : KSerializer<Uri> {
  297. override val descriptor: SerialDescriptor
  298. get() = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
  299. override fun deserialize(decoder: Decoder): Uri {
  300. return decoder.decodeString().toUri()
  301. }
  302. override fun serialize(encoder: Encoder, value: Uri) {
  303. encoder.encodeString(value.toString())
  304. }
  305. }
  306. @Serializable
  307. @Parcelize
  308. data class AttachmentItem(
  309. val name: String,
  310. val size: Long,
  311. val mimeType: String,
  312. @Serializable(with = UriSerializable::class)
  313. val uri: Uri
  314. ) : Parcelable
  315. @Serializable
  316. @Parcelize
  317. data class AttachmentItemList(
  318. val list: List<AttachmentItem?>
  319. ) : Parcelable
  320. val AttachmentItemListType = object : NavType<AttachmentItemList>(isNullableAllowed = false) {
  321. override fun put(bundle: Bundle, key: String, value: AttachmentItemList) {
  322. bundle.putParcelable(key, value)
  323. }
  324. override fun get(bundle: Bundle, key: String): AttachmentItemList? {
  325. return bundle.getParcelable<AttachmentItemList>(key)
  326. }
  327. override fun parseValue(value: String): AttachmentItemList {
  328. return Json.decodeFromString<AttachmentItemList>(value)
  329. }
  330. }
  331. fun AttachmentItemList.toArrayListUri() : ArrayList<Uri> {
  332. val list = arrayListOf<Uri>()
  333. this.list.forEach {
  334. it?.let {
  335. list.add(it.uri)
  336. }
  337. }
  338. return list
  339. }