mirror of
https://github.com/bbara04/Purefin.git
synced 2026-04-01 01:30: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:
@@ -0,0 +1,9 @@
|
||||
package hu.bbara.purefin.feature.shared.download
|
||||
|
||||
data class ActiveDownloadItem(
|
||||
val contentId: String,
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
val imageUrl: String,
|
||||
val progress: Float,
|
||||
)
|
||||
@@ -10,7 +10,9 @@ import hu.bbara.purefin.core.data.navigation.Route
|
||||
import hu.bbara.purefin.core.data.navigation.SeriesDto
|
||||
import hu.bbara.purefin.feature.download.MediaDownloadManager
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
@@ -25,36 +27,79 @@ class DownloadsViewModel @Inject constructor(
|
||||
|
||||
fun onMovieSelected(movieId: UUID) {
|
||||
navigationManager.navigate(Route.MovieRoute(
|
||||
MovieDto(
|
||||
id = movieId,
|
||||
)
|
||||
MovieDto(id = movieId)
|
||||
))
|
||||
}
|
||||
|
||||
fun onSeriesSelected(seriesId: UUID) {
|
||||
viewModelScope.launch {
|
||||
navigationManager.navigate(Route.SeriesRoute(
|
||||
SeriesDto(
|
||||
id = seriesId,
|
||||
)
|
||||
SeriesDto(id = seriesId)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Shared polling source: contentId → progress (0–100f). Starts when UI is subscribed.
|
||||
private val activeDownloadsMap = downloadManager.observeActiveDownloads()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyMap())
|
||||
|
||||
/** Items that are fully downloaded and not currently in progress. */
|
||||
val downloads = combine(
|
||||
offlineMediaRepository.movies,
|
||||
offlineMediaRepository.series,
|
||||
activeDownloadsMap
|
||||
) { movies, series, inProgress ->
|
||||
movies.values
|
||||
.filter { it.id.toString() !in inProgress }
|
||||
.map { PosterItem(type = BaseItemKind.MOVIE, movie = it) } +
|
||||
series.values.map { PosterItem(type = BaseItemKind.SERIES, series = it) }
|
||||
}
|
||||
|
||||
/** Items currently being downloaded with their progress. */
|
||||
val activeDownloads = combine(
|
||||
activeDownloadsMap,
|
||||
offlineMediaRepository.movies,
|
||||
offlineMediaRepository.episodes,
|
||||
offlineMediaRepository.series
|
||||
) { movies, series ->
|
||||
movies.values.map {
|
||||
PosterItem(
|
||||
type = BaseItemKind.MOVIE,
|
||||
movie = it
|
||||
)
|
||||
} + series.values.map {
|
||||
PosterItem(
|
||||
type = BaseItemKind.SERIES,
|
||||
series = it
|
||||
)
|
||||
) { inProgress, movies, episodes, seriesMap ->
|
||||
inProgress.mapNotNull { (contentId, progress) ->
|
||||
val id = try { UUID.fromString(contentId) } catch (e: Exception) { return@mapNotNull null }
|
||||
val movie = movies[id]
|
||||
if (movie != null) {
|
||||
ActiveDownloadItem(
|
||||
contentId = contentId,
|
||||
title = movie.title,
|
||||
subtitle = "",
|
||||
imageUrl = movie.heroImageUrl,
|
||||
progress = progress
|
||||
)
|
||||
} else {
|
||||
val episode = episodes[id]
|
||||
episode?.let {
|
||||
ActiveDownloadItem(
|
||||
contentId = contentId,
|
||||
title = it.title,
|
||||
subtitle = seriesMap[it.seriesId]?.name ?: "",
|
||||
imageUrl = it.heroImageUrl,
|
||||
progress = progress
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
|
||||
fun cancelDownload(contentId: String) {
|
||||
viewModelScope.launch {
|
||||
val id = try {
|
||||
UUID.fromString(contentId)
|
||||
} catch (e: Exception) {
|
||||
return@launch
|
||||
}
|
||||
if (offlineMediaRepository.episodes.value.containsKey(id)) {
|
||||
downloadManager.cancelEpisodeDownload(id)
|
||||
} else {
|
||||
downloadManager.cancelDownload(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user