feat: show in-progress downloads with progress bar and cancel in DownloadsContent

- Add ActiveDownloadItem data class to represent a download in progress
- Add observeActiveDownloads() to MediaDownloadManager, polling the Media3
  download index every 500ms on Dispatchers.IO for reliable real-time progress
  (listener callbacks alone do not fire on every progress update)
- DownloadsViewModel exposes activeDownloads (StateFlow) and cancelDownload();
  the completed downloads flow filters out items currently in progress
- DownloadsContent shows a "Downloading" section with thumbnail, title,
  progress bar + percentage, and a cancel button above the completed grid
This commit is contained in:
2026-03-03 14:08:19 +01:00
parent fce5a981a2
commit 2a7874806d
4 changed files with 235 additions and 21 deletions

View File

@@ -4,23 +4,41 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import hu.bbara.purefin.common.ui.PosterCard
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.feature.shared.download.ActiveDownloadItem
import hu.bbara.purefin.feature.shared.download.DownloadsViewModel
@Composable
@@ -29,8 +47,11 @@ fun DownloadsContent(
viewModel: DownloadsViewModel = hiltViewModel(),
) {
val downloads = viewModel.downloads.collectAsState(emptyList())
val activeDownloads = viewModel.activeDownloads.collectAsState()
if (downloads.value.isEmpty()) {
val isEmpty = downloads.value.isEmpty() && activeDownloads.value.isEmpty()
if (isEmpty) {
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
@@ -46,9 +67,9 @@ fun DownloadsContent(
text = "No downloads yet",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
)
}
return
}
LazyVerticalGrid(
@@ -58,6 +79,37 @@ fun DownloadsContent(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier.background(MaterialTheme.colorScheme.background)
) {
if (activeDownloads.value.isNotEmpty()) {
item(span = { GridItemSpan(maxLineSpan) }) {
Text(
text = "Downloading",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onBackground,
)
}
items(
items = activeDownloads.value,
key = { it.contentId },
span = { GridItemSpan(maxLineSpan) }
) { item ->
DownloadingItemRow(
item = item,
onCancel = { viewModel.cancelDownload(it) }
)
}
if (downloads.value.isNotEmpty()) {
item(span = { GridItemSpan(maxLineSpan) }) {
Text(
text = "Downloaded",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
items(downloads.value) { item ->
PosterCard(
item = item,
@@ -67,5 +119,77 @@ fun DownloadsContent(
)
}
}
}
@Composable
private fun DownloadingItemRow(
item: ActiveDownloadItem,
onCancel: (String) -> Unit,
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
PurefinAsyncImage(
model = item.imageUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(width = 44.dp, height = 66.dp)
.clip(RoundedCornerShape(8.dp))
)
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp)
) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (item.subtitle.isNotEmpty()) {
Text(
text = item.subtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Spacer(modifier = Modifier.height(6.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
LinearProgressIndicator(
progress = { item.progress / 100f },
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
)
Text(
text = "${item.progress.toInt()}%",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
IconButton(onClick = { onCancel(item.contentId) }) {
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = "Cancel download",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}