ContentChat.kt 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. package io.nexilis.alpha.ui.components
  2. import android.graphics.BitmapFactory
  3. import androidx.compose.foundation.Image
  4. import androidx.compose.foundation.background
  5. import androidx.compose.foundation.layout.Column
  6. import androidx.compose.foundation.layout.Row
  7. import androidx.compose.foundation.layout.aspectRatio
  8. import androidx.compose.foundation.layout.fillMaxWidth
  9. import androidx.compose.foundation.layout.padding
  10. import androidx.compose.foundation.layout.size
  11. import androidx.compose.foundation.layout.widthIn
  12. import androidx.compose.foundation.shape.RoundedCornerShape
  13. import androidx.compose.material.icons.Icons
  14. import androidx.compose.material.icons.filled.DownloadForOffline
  15. import androidx.compose.material.icons.filled.Headset
  16. import androidx.compose.material.icons.filled.PlayArrow
  17. import androidx.compose.material3.CircularProgressIndicator
  18. import androidx.compose.material3.Icon
  19. import androidx.compose.material3.IconButton
  20. import androidx.compose.material3.LinearProgressIndicator
  21. import androidx.compose.material3.MaterialTheme
  22. import androidx.compose.material3.Text
  23. import androidx.compose.runtime.Composable
  24. import androidx.compose.runtime.getValue
  25. import androidx.compose.runtime.livedata.observeAsState
  26. import androidx.compose.ui.Alignment
  27. import androidx.compose.ui.Modifier
  28. import androidx.compose.ui.graphics.Color
  29. import androidx.compose.ui.graphics.graphicsLayer
  30. import androidx.compose.ui.graphics.vector.ImageVector
  31. import androidx.compose.ui.layout.ContentScale
  32. import androidx.compose.ui.platform.LocalContext
  33. import androidx.compose.ui.res.vectorResource
  34. import androidx.compose.ui.text.intl.Locale
  35. import androidx.compose.ui.text.style.TextOverflow
  36. import androidx.compose.ui.text.toUpperCase
  37. import androidx.compose.ui.unit.dp
  38. import androidx.core.content.FileProvider
  39. import androidx.exifinterface.media.ExifInterface
  40. import androidx.hilt.navigation.compose.hiltViewModel
  41. import coil.compose.rememberAsyncImagePainter
  42. import io.nexilis.alpha.R
  43. import io.nexilis.service.core.Orange
  44. import io.nexilis.service.core.extension
  45. import io.nexilis.service.core.toHumanStandard
  46. import io.nexilis.service.data.entities.Message
  47. import io.nexilis.service.data.viewmodels.ProgressViewModel
  48. import java.io.File
  49. import java.io.FileInputStream
  50. @Composable
  51. fun ContentChat(modifier: Modifier = Modifier, message: Message) {
  52. val context = LocalContext.current
  53. if (message.image_id.isNotEmpty()) {
  54. Column(modifier = Modifier.padding(2.dp)) {
  55. val file = File(context.filesDir, message.image_id)
  56. val uri = FileProvider.getUriForFile(
  57. context, context.applicationContext.packageName + ".provider", file
  58. )
  59. var width: Int
  60. var height: Int
  61. FileInputStream(file).use {
  62. val exif = ExifInterface(it)
  63. width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 1)
  64. height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 1)
  65. val orientation = exif.getAttributeInt(
  66. ExifInterface.TAG_ORIENTATION,
  67. ExifInterface.ORIENTATION_UNDEFINED
  68. )
  69. if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270) {
  70. height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 1)
  71. width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 1)
  72. }
  73. }
  74. if (width == 0) {
  75. FileInputStream(file).use {
  76. val bitmap = BitmapFactory.decodeStream(it)
  77. width = bitmap.width
  78. if (height == 0) height = bitmap.height
  79. }
  80. }
  81. val aspectRatio = width.toFloat() / height.toFloat()
  82. Image(
  83. painter = rememberAsyncImagePainter(uri),
  84. contentDescription = "",
  85. modifier = modifier
  86. .aspectRatio(aspectRatio)
  87. .graphicsLayer {
  88. shape = RoundedCornerShape(12.dp)
  89. clip = true
  90. },
  91. contentScale = ContentScale.Fit
  92. )
  93. if (message.message_text.isNotEmpty()) {
  94. Text(
  95. text = message.message_text,
  96. modifier = Modifier.padding(8.dp),
  97. color = MaterialTheme.colorScheme.onSurface,
  98. style = MaterialTheme.typography.bodyMedium,
  99. )
  100. }
  101. }
  102. } else if (message.file_id.isNotEmpty()) {
  103. Column(
  104. modifier = Modifier
  105. .padding(2.dp)
  106. .fillMaxWidth(0.65f)
  107. ) {
  108. Row(modifier = Modifier
  109. .graphicsLayer {
  110. shape = RoundedCornerShape(12.dp)
  111. clip = true
  112. }
  113. .align(Alignment.CenterHorizontally)
  114. .background(MaterialTheme.colorScheme.surface)
  115. .padding(8.dp)
  116. ) {
  117. FileType(
  118. message = message, modifier = Modifier
  119. .align(Alignment.CenterVertically)
  120. )
  121. Column(
  122. modifier = Modifier
  123. .weight(1f)
  124. ) {
  125. Text(
  126. text = message.message_text.split("|").first(),
  127. color = MaterialTheme.colorScheme.onSurface,
  128. style = MaterialTheme.typography.bodyMedium,
  129. maxLines = 2,
  130. overflow = TextOverflow.Ellipsis
  131. )
  132. FileDesc(file = message.file_id)
  133. }
  134. FileProgress(
  135. message = message,
  136. modifier = Modifier.align(Alignment.CenterVertically)
  137. )
  138. }
  139. val messageText = message.message_text.split("|").last()
  140. if (messageText.isNotEmpty()) {
  141. Text(
  142. text = messageText,
  143. modifier = Modifier.padding(8.dp),
  144. color = MaterialTheme.colorScheme.onSurface,
  145. style = MaterialTheme.typography.bodyMedium,
  146. )
  147. }
  148. }
  149. } else if (message.audio_id.isNotEmpty()) {
  150. Column(
  151. modifier = Modifier
  152. .padding(2.dp)
  153. .widthIn(max = 210.dp)
  154. ) {
  155. Row(modifier = Modifier
  156. .graphicsLayer {
  157. shape = RoundedCornerShape(12.dp)
  158. clip = true
  159. }
  160. .background(MaterialTheme.colorScheme.surface)) {
  161. Icon(
  162. modifier = Modifier
  163. .size(48.dp)
  164. .padding(start = 8.dp)
  165. .align(Alignment.CenterVertically),
  166. imageVector = Icons.Default.Headset,
  167. contentDescription = null,
  168. tint = Color.Orange
  169. )
  170. Column {
  171. Row(
  172. modifier = Modifier.padding(
  173. start = 4.dp,
  174. top = 8.dp,
  175. end = 8.dp,
  176. bottom = 8.dp
  177. )
  178. ) {
  179. Icon(
  180. modifier = Modifier
  181. .size(32.dp)
  182. .align(Alignment.CenterVertically),
  183. imageVector = Icons.Default.PlayArrow,
  184. contentDescription = null,
  185. tint = MaterialTheme.colorScheme.primary
  186. )
  187. LinearProgressIndicator(
  188. progress = { 0.5f },
  189. modifier = Modifier
  190. .align(Alignment.CenterVertically)
  191. .fillMaxWidth(),
  192. )
  193. }
  194. Text(
  195. modifier = Modifier.padding(
  196. start = 4.dp,
  197. top = 8.dp,
  198. end = 8.dp,
  199. bottom = 8.dp
  200. ),
  201. color = MaterialTheme.colorScheme.onSurface,
  202. text = message.message_text.split("|").first(),
  203. style = MaterialTheme.typography.labelSmall,
  204. maxLines = 1,
  205. overflow = TextOverflow.Ellipsis
  206. )
  207. }
  208. }
  209. val messageText = message.message_text.split("|").last()
  210. if (messageText.isNotEmpty()) {
  211. Text(
  212. text = messageText,
  213. modifier = Modifier.padding(8.dp),
  214. color = MaterialTheme.colorScheme.onSurface,
  215. style = MaterialTheme.typography.bodyMedium,
  216. )
  217. }
  218. }
  219. } else {
  220. Text(
  221. text = message.message_text,
  222. modifier = Modifier.padding(8.dp),
  223. color = MaterialTheme.colorScheme.onSurface,
  224. style = MaterialTheme.typography.bodyMedium,
  225. )
  226. }
  227. }
  228. @Composable
  229. fun FileType(message: Message, modifier: Modifier) {
  230. val imageVector = when (message.file_id.extension) {
  231. "apk", "aab" -> ImageVector.vectorResource(id = R.drawable.ic_apk)
  232. "aac", "mp3", "m4a", "ogg" -> ImageVector.vectorResource(id = R.drawable.ic_audio)
  233. "xls", "xlsx" -> ImageVector.vectorResource(id = R.drawable.ic_excel)
  234. "jpg", "jpeg", "webp", "png", "svg" -> ImageVector.vectorResource(id = R.drawable.ic_image)
  235. "pdf" -> ImageVector.vectorResource(id = R.drawable.ic_pdf)
  236. "ppt", "pptx" -> ImageVector.vectorResource(id = R.drawable.ic_ppt)
  237. "txt" -> ImageVector.vectorResource(id = R.drawable.ic_txt)
  238. "mp4", "mov", "3gp" -> ImageVector.vectorResource(id = R.drawable.ic_video)
  239. "doc", "docx" -> ImageVector.vectorResource(id = R.drawable.ic_word)
  240. "zip", "gz", "rar" -> ImageVector.vectorResource(id = R.drawable.ic_zip)
  241. else -> ImageVector.vectorResource(id = R.drawable.ic_file)
  242. }
  243. Image(
  244. modifier = modifier
  245. .size(48.dp),
  246. imageVector = imageVector,
  247. contentDescription = null
  248. )
  249. }
  250. @Composable
  251. fun FileDesc(file: String, modifier: Modifier = Modifier) {
  252. val context = LocalContext.current
  253. val f = File(context.filesDir, file)
  254. if (f.exists()) {
  255. Text(
  256. modifier = modifier,
  257. text = "${
  258. f.length().toHumanStandard()
  259. } • ${file.extension.toUpperCase(Locale.current)}",
  260. color = MaterialTheme.colorScheme.onSurface,
  261. style = MaterialTheme.typography.labelSmall,
  262. )
  263. }
  264. }
  265. @Composable
  266. fun FileProgress(message: Message, modifier: Modifier = Modifier) {
  267. val context = LocalContext.current
  268. val progressViewModel: ProgressViewModel = hiltViewModel()
  269. val progress by progressViewModel.get(message.message_id).observeAsState()
  270. progress?.let {
  271. if (it.value > 0f && it.value < 1f) {
  272. CircularProgressIndicator(
  273. progress = { it.value },
  274. modifier = modifier
  275. .size(32.dp),
  276. )
  277. }
  278. }
  279. val file = File(context.filesDir, message.file_id)
  280. if (!file.exists()) {
  281. IconButton(
  282. onClick = { /*TODO*/ },
  283. modifier = modifier
  284. ) {
  285. Icon(
  286. imageVector = Icons.Default.DownloadForOffline,
  287. contentDescription = null,
  288. tint = Color.LightGray
  289. )
  290. }
  291. }
  292. }