feat: Refactor attachment components to adopt Telegram-style UI for images and files

This commit is contained in:
k1ngsterr1
2026-01-24 15:03:23 +05:00
parent fa40f8a535
commit d5083e60a5
2 changed files with 226 additions and 235 deletions

View File

@@ -90,6 +90,9 @@ dependencies {
implementation("io.coil-kt:coil-compose:2.5.0") implementation("io.coil-kt:coil-compose:2.5.0")
implementation("io.coil-kt:coil-gif:2.5.0") // For animated WebP/GIF support implementation("io.coil-kt:coil-gif:2.5.0") // For animated WebP/GIF support
// Blurhash for image placeholders
implementation("com.vanniktech:blurhash:0.1.0")
// Crypto libraries for key generation // Crypto libraries for key generation
implementation("org.bitcoinj:bitcoinj-core:0.16.2") implementation("org.bitcoinj:bitcoinj-core:0.16.2")
implementation("org.bouncycastle:bcprov-jdk15to18:1.77") implementation("org.bouncycastle:bcprov-jdk15to18:1.77")

View File

@@ -105,7 +105,7 @@ fun MessageAttachments(
} }
/** /**
* Image attachment * Image attachment - Telegram style
*/ */
@Composable @Composable
fun ImageAttachment( fun ImageAttachment(
@@ -114,26 +114,20 @@ fun ImageAttachment(
privateKey: String, privateKey: String,
isDarkTheme: Boolean isDarkTheme: Boolean
) { ) {
val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) } var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
var imageBitmap by remember { mutableStateOf<Bitmap?>(null) } var imageBitmap by remember { mutableStateOf<Bitmap?>(null) }
var downloadProgress by remember { mutableStateOf(0) }
// Проверяем статус при загрузке
LaunchedEffect(attachment.id) { LaunchedEffect(attachment.id) {
downloadStatus = if (attachment.blob.isNotEmpty() && !isDownloadTag(attachment.preview)) { downloadStatus = if (attachment.blob.isNotEmpty() && !isDownloadTag(attachment.preview)) {
// Blob уже есть - сразу декодируем
DownloadStatus.DOWNLOADED DownloadStatus.DOWNLOADED
} else if (isDownloadTag(attachment.preview)) { } else if (isDownloadTag(attachment.preview)) {
// Нужно скачать с сервера
DownloadStatus.NOT_DOWNLOADED DownloadStatus.NOT_DOWNLOADED
} else { } else {
DownloadStatus.DOWNLOADED DownloadStatus.DOWNLOADED
} }
// Если скачано - декодируем изображение
if (downloadStatus == DownloadStatus.DOWNLOADED && attachment.blob.isNotEmpty()) { if (downloadStatus == DownloadStatus.DOWNLOADED && attachment.blob.isNotEmpty()) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
imageBitmap = base64ToBitmap(attachment.blob) imageBitmap = base64ToBitmap(attachment.blob)
@@ -145,19 +139,15 @@ fun ImageAttachment(
scope.launch { scope.launch {
try { try {
downloadStatus = DownloadStatus.DOWNLOADING downloadStatus = DownloadStatus.DOWNLOADING
val tag = getDownloadTag(attachment.preview) val tag = getDownloadTag(attachment.preview)
if (tag.isEmpty()) { if (tag.isEmpty()) {
downloadStatus = DownloadStatus.ERROR downloadStatus = DownloadStatus.ERROR
return@launch return@launch
} }
// Скачиваем с транспортного сервера
val encryptedContent = TransportManager.downloadFile(attachment.id, tag) val encryptedContent = TransportManager.downloadFile(attachment.id, tag)
downloadStatus = DownloadStatus.DECRYPTING downloadStatus = DownloadStatus.DECRYPTING
// Расшифровываем с ChaCha ключом
val decrypted = MessageCrypto.decryptAttachmentBlob( val decrypted = MessageCrypto.decryptAttachmentBlob(
encryptedContent, encryptedContent,
chachaKey, chachaKey,
@@ -179,71 +169,64 @@ fun ImageAttachment(
} }
} }
Card( // Telegram-style: Изображение без Card wrapper
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .widthIn(min = 200.dp, max = 280.dp)
.clip(RoundedCornerShape(12.dp)) .heightIn(min = 150.dp, max = 350.dp)
.clip(RoundedCornerShape(8.dp))
.clickable { .clickable {
if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) { if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) {
download() download()
} }
}, },
colors = CardDefaults.cardColors( contentAlignment = Alignment.Center
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8)
)
) { ) {
Box( when (downloadStatus) {
modifier = Modifier DownloadStatus.DOWNLOADED -> {
.fillMaxWidth() imageBitmap?.let { bitmap ->
.heightIn(min = 100.dp, max = 300.dp), Image(
contentAlignment = Alignment.Center bitmap = bitmap.asImageBitmap(),
) { contentDescription = "Image",
when (downloadStatus) { modifier = Modifier.fillMaxSize(),
DownloadStatus.DOWNLOADED -> { contentScale = ContentScale.Crop
imageBitmap?.let { bitmap -> )
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = "Image",
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Fit
)
}
} }
DownloadStatus.NOT_DOWNLOADED -> { }
// Показываем preview (blurhash) если есть DownloadStatus.NOT_DOWNLOADED -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE0E0E0)),
contentAlignment = Alignment.Center
) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(16.dp)
) { ) {
Icon( Icon(
Icons.Default.Image, Icons.Default.Download,
contentDescription = null, contentDescription = null,
tint = if (isDarkTheme) Color.White.copy(alpha = 0.7f) else Color.Gray, tint = PrimaryBlue,
modifier = Modifier.size(48.dp) modifier = Modifier.size(40.dp)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Button( Text(
onClick = download, "Tap to download",
colors = ButtonDefaults.buttonColors( fontSize = 13.sp,
containerColor = PrimaryBlue color = if (isDarkTheme) Color.White.copy(0.7f) else Color.Gray
) )
) {
Icon(
Icons.Default.Download,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Download")
}
} }
} }
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> { }
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE0E0E0)),
contentAlignment = Alignment.Center
) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(16.dp)
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
color = PrimaryBlue, color = PrimaryBlue,
@@ -253,32 +236,44 @@ fun ImageAttachment(
Text( Text(
text = if (downloadStatus == DownloadStatus.DECRYPTING) "Decrypting..." else "Downloading...", text = if (downloadStatus == DownloadStatus.DECRYPTING) "Decrypting..." else "Downloading...",
fontSize = 12.sp, fontSize = 12.sp,
color = if (isDarkTheme) Color.White.copy(alpha = 0.7f) else Color.Gray color = if (isDarkTheme) Color.White.copy(0.7f) else Color.Gray
) )
} }
} }
DownloadStatus.ERROR -> { }
DownloadStatus.ERROR -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE0E0E0)),
contentAlignment = Alignment.Center
) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(16.dp)
) { ) {
Icon( Icon(
Icons.Default.Error, Icons.Default.Error,
contentDescription = null, contentDescription = null,
tint = Color.Red, tint = Color(0xFFE53935),
modifier = Modifier.size(32.dp) modifier = Modifier.size(32.dp)
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text("Download failed", color = Color.Red, fontSize = 12.sp) Text("Failed", fontSize = 12.sp, color = Color(0xFFE53935))
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(4.dp))
TextButton(onClick = download) { TextButton(onClick = download) {
Text("Retry") Text("Retry", fontSize = 12.sp, color = PrimaryBlue)
} }
} }
} }
else -> { }
CircularProgressIndicator(modifier = Modifier.size(24.dp)) else -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE0E0E0)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(modifier = Modifier.size(24.dp), color = PrimaryBlue)
} }
} }
} }
@@ -286,7 +281,7 @@ fun ImageAttachment(
} }
/** /**
* File attachment * File attachment - Telegram style
*/ */
@Composable @Composable
fun FileAttachment( fun FileAttachment(
@@ -298,12 +293,10 @@ fun FileAttachment(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) } var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
// Парсим метаданные: "filesize::filename"
val preview = attachment.preview val preview = attachment.preview
val parts = preview.split("::") val parts = preview.split("::")
val (fileSize, fileName) = when { val (fileSize, fileName) = when {
parts.size >= 3 -> { parts.size >= 3 -> {
// Формат: "UUID::filesize::filename"
val size = parts[1].toLongOrNull() ?: 0L val size = parts[1].toLongOrNull() ?: 0L
val name = parts.drop(2).joinToString("::") val name = parts.drop(2).joinToString("::")
Pair(size, name) Pair(size, name)
@@ -315,7 +308,6 @@ fun FileAttachment(
else -> Pair(0L, "File") else -> Pair(0L, "File")
} }
// Проверяем статус
LaunchedEffect(attachment.id) { LaunchedEffect(attachment.id) {
downloadStatus = if (isDownloadTag(preview)) { downloadStatus = if (isDownloadTag(preview)) {
DownloadStatus.NOT_DOWNLOADED DownloadStatus.NOT_DOWNLOADED
@@ -328,7 +320,6 @@ fun FileAttachment(
scope.launch { scope.launch {
try { try {
downloadStatus = DownloadStatus.DOWNLOADING downloadStatus = DownloadStatus.DOWNLOADING
val tag = getDownloadTag(preview) val tag = getDownloadTag(preview)
if (tag.isEmpty()) { if (tag.isEmpty()) {
downloadStatus = DownloadStatus.ERROR downloadStatus = DownloadStatus.ERROR
@@ -336,7 +327,6 @@ fun FileAttachment(
} }
val encryptedContent = TransportManager.downloadFile(attachment.id, tag) val encryptedContent = TransportManager.downloadFile(attachment.id, tag)
downloadStatus = DownloadStatus.DECRYPTING downloadStatus = DownloadStatus.DECRYPTING
val decrypted = MessageCrypto.decryptAttachmentBlob( val decrypted = MessageCrypto.decryptAttachmentBlob(
@@ -346,7 +336,6 @@ fun FileAttachment(
) )
if (decrypted != null) { if (decrypted != null) {
// TODO: Сохранить файл в Downloads
downloadStatus = DownloadStatus.DOWNLOADED downloadStatus = DownloadStatus.DOWNLOADED
} else { } else {
downloadStatus = DownloadStatus.ERROR downloadStatus = DownloadStatus.ERROR
@@ -358,102 +347,97 @@ fun FileAttachment(
} }
} }
Card( // Telegram-style: компактный row с иконкой файла
Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(8.dp))
.background(if (isDarkTheme) Color(0xFF1F1F1F) else Color.White.copy(0.3f))
.clickable { .clickable {
if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) { if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) {
download() download()
} }
}, }
colors = CardDefaults.cardColors( .padding(10.dp),
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8) verticalAlignment = Alignment.CenterVertically
)
) { ) {
Row( // File icon
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .size(44.dp)
.padding(12.dp), .clip(RoundedCornerShape(6.dp))
verticalAlignment = Alignment.CenterVertically .background(PrimaryBlue.copy(alpha = 0.15f)),
contentAlignment = Alignment.Center
) { ) {
// File icon Icon(
Box( Icons.Default.InsertDriveFile,
modifier = Modifier contentDescription = null,
.size(48.dp) tint = PrimaryBlue,
.clip(RoundedCornerShape(8.dp)) modifier = Modifier.size(22.dp)
.background(PrimaryBlue.copy(alpha = 0.2f)), )
contentAlignment = Alignment.Center }
) {
Spacer(modifier = Modifier.width(10.dp))
// File info
Column(modifier = Modifier.weight(1f)) {
Text(
text = fileName,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = if (isDarkTheme) Color.White else Color.Black,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = formatFileSize(fileSize),
fontSize = 12.sp,
color = if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Gray
)
}
Spacer(modifier = Modifier.width(8.dp))
// Download/status icon
when (downloadStatus) {
DownloadStatus.NOT_DOWNLOADED -> {
Icon( Icon(
Icons.Default.InsertDriveFile, Icons.Default.Download,
contentDescription = null, contentDescription = "Download",
tint = PrimaryBlue, tint = PrimaryBlue,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
} }
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
Spacer(modifier = Modifier.width(12.dp)) CircularProgressIndicator(
modifier = Modifier.size(20.dp),
// File info color = PrimaryBlue,
Column(modifier = Modifier.weight(1f)) { strokeWidth = 2.dp
Text(
text = fileName,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = if (isDarkTheme) Color.White else Color.Black,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = formatFileSize(fileSize),
fontSize = 12.sp,
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Gray
) )
} }
DownloadStatus.DOWNLOADED -> {
// Download button/status Icon(
when (downloadStatus) { Icons.Default.CheckCircle,
DownloadStatus.NOT_DOWNLOADED -> { contentDescription = "Downloaded",
IconButton(onClick = download) { tint = Color(0xFF4CAF50),
Icon( modifier = Modifier.size(24.dp)
Icons.Default.Download, )
contentDescription = "Download",
tint = PrimaryBlue
)
}
}
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = PrimaryBlue,
strokeWidth = 2.dp
)
}
DownloadStatus.DOWNLOADED -> {
Icon(
Icons.Default.CheckCircle,
contentDescription = "Downloaded",
tint = Color(0xFF4CAF50)
)
}
DownloadStatus.ERROR -> {
IconButton(onClick = download) {
Icon(
Icons.Default.Refresh,
contentDescription = "Retry",
tint = Color.Red
)
}
}
else -> {}
} }
DownloadStatus.ERROR -> {
Icon(
Icons.Default.Refresh,
contentDescription = "Retry",
tint = Color(0xFFE53935),
modifier = Modifier.size(24.dp)
)
}
else -> {}
} }
} }
} }
/** /**
* Avatar attachment - аватар отправителя * Avatar attachment - Telegram style (более компактный)
*/ */
@Composable @Composable
fun AvatarAttachment( fun AvatarAttachment(
@@ -464,7 +448,6 @@ fun AvatarAttachment(
avatarRepository: AvatarRepository?, avatarRepository: AvatarRepository?,
isDarkTheme: Boolean isDarkTheme: Boolean
) { ) {
val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) } var downloadStatus by remember { mutableStateOf(DownloadStatus.PENDING) }
@@ -490,7 +473,6 @@ fun AvatarAttachment(
scope.launch { scope.launch {
try { try {
downloadStatus = DownloadStatus.DOWNLOADING downloadStatus = DownloadStatus.DOWNLOADING
val tag = getDownloadTag(attachment.preview) val tag = getDownloadTag(attachment.preview)
if (tag.isEmpty()) { if (tag.isEmpty()) {
downloadStatus = DownloadStatus.ERROR downloadStatus = DownloadStatus.ERROR
@@ -498,7 +480,6 @@ fun AvatarAttachment(
} }
val encryptedContent = TransportManager.downloadFile(attachment.id, tag) val encryptedContent = TransportManager.downloadFile(attachment.id, tag)
downloadStatus = DownloadStatus.DECRYPTING downloadStatus = DownloadStatus.DECRYPTING
val decrypted = MessageCrypto.decryptAttachmentBlob( val decrypted = MessageCrypto.decryptAttachmentBlob(
@@ -511,7 +492,6 @@ fun AvatarAttachment(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
avatarBitmap = base64ToBitmap(decrypted) avatarBitmap = base64ToBitmap(decrypted)
} }
// Сохраняем аватар в кэш
avatarRepository?.saveAvatar(senderPublicKey, decrypted) avatarRepository?.saveAvatar(senderPublicKey, decrypted)
downloadStatus = DownloadStatus.DOWNLOADED downloadStatus = DownloadStatus.DOWNLOADED
} else { } else {
@@ -524,99 +504,107 @@ fun AvatarAttachment(
} }
} }
Card( // Telegram-style: компактный row с круглым превью
Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(12.dp)), .clip(RoundedCornerShape(8.dp))
colors = CardDefaults.cardColors( .background(if (isDarkTheme) Color(0xFF1F1F1F) else Color.White.copy(0.3f))
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8) .clickable(enabled = downloadStatus == DownloadStatus.NOT_DOWNLOADED) {
) download()
}
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
Row( // Avatar preview (круглое)
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .size(50.dp)
.padding(12.dp), .clip(CircleShape)
verticalAlignment = Alignment.CenterVertically .background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE0E0E0)),
contentAlignment = Alignment.Center
) { ) {
// Avatar preview when (downloadStatus) {
Box( DownloadStatus.DOWNLOADED -> {
modifier = Modifier avatarBitmap?.let { bitmap ->
.size(60.dp) Image(
.clip(CircleShape) bitmap = bitmap.asImageBitmap(),
.background(if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFD0D0D0)), contentDescription = "Avatar",
contentAlignment = Alignment.Center modifier = Modifier.fillMaxSize(),
) { contentScale = ContentScale.Crop
when (downloadStatus) {
DownloadStatus.DOWNLOADED -> {
avatarBitmap?.let { bitmap ->
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = "Avatar",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = PrimaryBlue,
strokeWidth = 2.dp
)
}
else -> {
Icon(
Icons.Default.Person,
contentDescription = null,
tint = if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Gray,
modifier = Modifier.size(32.dp)
) )
} }
} }
DownloadStatus.DOWNLOADING, DownloadStatus.DECRYPTING -> {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = PrimaryBlue,
strokeWidth = 2.dp
)
}
else -> {
Icon(
Icons.Default.Person,
contentDescription = null,
tint = if (isDarkTheme) Color.White.copy(0.4f) else Color.Gray,
modifier = Modifier.size(28.dp)
)
}
} }
}
Spacer(modifier = Modifier.width(12.dp))
Spacer(modifier = Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) {
Text( Row(verticalAlignment = Alignment.CenterVertically) {
text = "Avatar",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = if (isDarkTheme) Color.White else Color.Black
)
Spacer(modifier = Modifier.width(4.dp))
Icon(
Icons.Default.Lock,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(14.dp)
)
}
Text( Text(
text = "An avatar image shared in the message.", text = "Avatar",
fontSize = 12.sp, fontSize = 14.sp,
color = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Gray fontWeight = FontWeight.Medium,
color = if (isDarkTheme) Color.White else Color.Black
)
Spacer(modifier = Modifier.width(4.dp))
Icon(
Icons.Default.Lock,
contentDescription = null,
tint = if (isDarkTheme) Color.White.copy(0.5f) else Color.Gray,
modifier = Modifier.size(12.dp)
) )
} }
Text(
if (downloadStatus == DownloadStatus.NOT_DOWNLOADED) { text = "Profile photo shared",
Button( fontSize = 12.sp,
onClick = download, color = if (isDarkTheme) Color.White.copy(0.5f) else Color.Gray
colors = ButtonDefaults.buttonColors( )
containerColor = PrimaryBlue }
),
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 6.dp) // Download/status icon
) { when (downloadStatus) {
Icon( DownloadStatus.NOT_DOWNLOADED -> {
Icons.Default.Download, Icon(
contentDescription = null, Icons.Default.Download,
modifier = Modifier.size(16.dp) contentDescription = "Download",
) tint = PrimaryBlue,
Spacer(modifier = Modifier.width(4.dp)) modifier = Modifier.size(24.dp)
Text("Download", fontSize = 12.sp) )
}
} }
DownloadStatus.DOWNLOADED -> {
Icon(
Icons.Default.CheckCircle,
contentDescription = "Downloaded",
tint = Color(0xFF4CAF50),
modifier = Modifier.size(24.dp)
)
}
DownloadStatus.ERROR -> {
Icon(
Icons.Default.Refresh,
contentDescription = "Retry",
tint = Color(0xFFE53935),
modifier = Modifier.size(24.dp)
)
}
else -> {}
} }
} }
} }