Attachments.kt 16 KB

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