mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
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:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user