mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
feat: Implement Episode download functionality
This commit is contained in:
@@ -17,7 +17,9 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material.icons.outlined.ArrowBack
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.Cast
|
import androidx.compose.material.icons.outlined.Cast
|
||||||
|
import androidx.compose.material.icons.outlined.Close
|
||||||
import androidx.compose.material.icons.outlined.Download
|
import androidx.compose.material.icons.outlined.Download
|
||||||
|
import androidx.compose.material.icons.outlined.DownloadDone
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -38,6 +40,7 @@ import hu.bbara.purefin.common.ui.components.MediaActionButton
|
|||||||
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
|
import hu.bbara.purefin.common.ui.components.MediaPlaybackSettings
|
||||||
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
import hu.bbara.purefin.common.ui.components.MediaResumeButton
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
|
import hu.bbara.purefin.feature.download.DownloadState
|
||||||
import hu.bbara.purefin.player.PlayerActivity
|
import hu.bbara.purefin.player.PlayerActivity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -69,6 +72,8 @@ internal fun EpisodeTopBar(
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun EpisodeDetails(
|
internal fun EpisodeDetails(
|
||||||
episode: Episode,
|
episode: Episode,
|
||||||
|
downloadState: DownloadState,
|
||||||
|
onDownloadClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
@@ -153,8 +158,14 @@ internal fun EpisodeDetails(
|
|||||||
MediaActionButton(
|
MediaActionButton(
|
||||||
backgroundColor = MaterialTheme.colorScheme.secondary,
|
backgroundColor = MaterialTheme.colorScheme.secondary,
|
||||||
iconColor = MaterialTheme.colorScheme.onSecondary,
|
iconColor = MaterialTheme.colorScheme.onSecondary,
|
||||||
icon = Icons.Outlined.Download,
|
icon = when (downloadState) {
|
||||||
height = 48.dp
|
is DownloadState.NotDownloaded -> Icons.Outlined.Download
|
||||||
|
is DownloadState.Downloading -> Icons.Outlined.Close
|
||||||
|
is DownloadState.Downloaded -> Icons.Outlined.DownloadDone
|
||||||
|
is DownloadState.Failed -> Icons.Outlined.Download
|
||||||
|
},
|
||||||
|
height = 48.dp,
|
||||||
|
onClick = onDownloadClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import hu.bbara.purefin.common.ui.PurefinWaitingScreen
|
|||||||
import hu.bbara.purefin.common.ui.components.MediaHero
|
import hu.bbara.purefin.common.ui.components.MediaHero
|
||||||
import hu.bbara.purefin.core.data.navigation.EpisodeDto
|
import hu.bbara.purefin.core.data.navigation.EpisodeDto
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
|
import hu.bbara.purefin.feature.download.DownloadState
|
||||||
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
import hu.bbara.purefin.feature.shared.content.episode.EpisodeScreenViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -36,6 +37,7 @@ fun EpisodeScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val episode = viewModel.episode.collectAsState()
|
val episode = viewModel.episode.collectAsState()
|
||||||
|
val downloadState = viewModel.downloadState.collectAsState()
|
||||||
|
|
||||||
if (episode.value == null) {
|
if (episode.value == null) {
|
||||||
PurefinWaitingScreen()
|
PurefinWaitingScreen()
|
||||||
@@ -44,7 +46,9 @@ fun EpisodeScreen(
|
|||||||
|
|
||||||
EpisodeScreenInternal(
|
EpisodeScreenInternal(
|
||||||
episode = episode.value!!,
|
episode = episode.value!!,
|
||||||
|
downloadState = downloadState.value,
|
||||||
onBack = viewModel::onBack,
|
onBack = viewModel::onBack,
|
||||||
|
onDownloadClick = viewModel::onDownloadClick,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -52,7 +56,9 @@ fun EpisodeScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun EpisodeScreenInternal(
|
private fun EpisodeScreenInternal(
|
||||||
episode: Episode,
|
episode: Episode,
|
||||||
|
downloadState: DownloadState,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
|
onDownloadClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -79,6 +85,8 @@ private fun EpisodeScreenInternal(
|
|||||||
)
|
)
|
||||||
EpisodeDetails(
|
EpisodeDetails(
|
||||||
episode = episode,
|
episode = episode,
|
||||||
|
downloadState = downloadState,
|
||||||
|
onDownloadClick = onDownloadClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
|
|||||||
@@ -213,18 +213,18 @@ class InMemoryAppContentRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadMovie(movie: Movie): Movie {
|
suspend fun loadMovie(movieId: UUID): Movie {
|
||||||
val movieItem = jellyfinApiClient.getItemInfo(movie.id)
|
val movieItem = jellyfinApiClient.getItemInfo(movieId)
|
||||||
?: throw RuntimeException("Movie not found")
|
?: throw RuntimeException("Movie not found")
|
||||||
val updatedMovie = movieItem.toMovie(serverUrl(), movie.libraryId)
|
val updatedMovie = movieItem.toMovie(serverUrl(), movieItem.parentId!!)
|
||||||
mediaRepository._movies.update { it + (updatedMovie.id to updatedMovie) }
|
mediaRepository._movies.update { it + (updatedMovie.id to updatedMovie) }
|
||||||
return updatedMovie
|
return updatedMovie
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadSeries(series: Series): Series {
|
suspend fun loadSeries(seriesId: UUID): Series {
|
||||||
val seriesItem = jellyfinApiClient.getItemInfo(series.id)
|
val seriesItem = jellyfinApiClient.getItemInfo(seriesId)
|
||||||
?: throw RuntimeException("Series not found")
|
?: throw RuntimeException("Series not found")
|
||||||
val updatedSeries = seriesItem.toSeries(serverUrl(), series.libraryId)
|
val updatedSeries = seriesItem.toSeries(serverUrl(), seriesItem.parentId!!)
|
||||||
mediaRepository._series.update { it + (updatedSeries.id to updatedSeries) }
|
mediaRepository._series.update { it + (updatedSeries.id to updatedSeries) }
|
||||||
return updatedSeries
|
return updatedSeries
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,10 @@ interface EpisodeDao {
|
|||||||
|
|
||||||
@Query("DELETE FROM episodes WHERE seasonId = :seasonId")
|
@Query("DELETE FROM episodes WHERE seasonId = :seasonId")
|
||||||
suspend fun deleteBySeasonId(seasonId: UUID)
|
suspend fun deleteBySeasonId(seasonId: UUID)
|
||||||
|
|
||||||
|
@Query("DELETE FROM episodes WHERE id = :id")
|
||||||
|
suspend fun deleteById(id: UUID)
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM episodes WHERE seasonId = :seasonId")
|
||||||
|
suspend fun countBySeasonId(seasonId: UUID): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,4 +25,10 @@ interface SeasonDao {
|
|||||||
|
|
||||||
@Query("DELETE FROM seasons WHERE seriesId = :seriesId")
|
@Query("DELETE FROM seasons WHERE seriesId = :seriesId")
|
||||||
suspend fun deleteBySeriesId(seriesId: UUID)
|
suspend fun deleteBySeriesId(seriesId: UUID)
|
||||||
|
|
||||||
|
@Query("DELETE FROM seasons WHERE id = :id")
|
||||||
|
suspend fun deleteById(id: UUID)
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM seasons WHERE seriesId = :seriesId")
|
||||||
|
suspend fun countBySeriesId(seriesId: UUID): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,7 @@ interface SeriesDao {
|
|||||||
|
|
||||||
@Query("DELETE FROM series")
|
@Query("DELETE FROM series")
|
||||||
suspend fun clear()
|
suspend fun clear()
|
||||||
|
|
||||||
|
@Query("DELETE FROM series WHERE id = :id")
|
||||||
|
suspend fun deleteById(id: UUID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun saveSeason(season: Season) {
|
||||||
|
database.withTransaction {
|
||||||
|
seriesDao.getById(season.seriesId)
|
||||||
|
?: throw RuntimeException("Cannot add season without series. Season: $season")
|
||||||
|
seasonDao.upsert(season.toEntity())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun saveEpisode(episode: Episode) {
|
suspend fun saveEpisode(episode: Episode) {
|
||||||
database.withTransaction {
|
database.withTransaction {
|
||||||
seriesDao.getById(episode.seriesId)
|
seriesDao.getById(episode.seriesId)
|
||||||
@@ -89,6 +97,23 @@ class OfflineRoomMediaLocalDataSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun deleteEpisodeAndCleanup(episodeId: UUID) {
|
||||||
|
database.withTransaction {
|
||||||
|
val episode = episodeDao.getById(episodeId) ?: return@withTransaction
|
||||||
|
episodeDao.deleteById(episodeId)
|
||||||
|
|
||||||
|
val remainingEpisodesInSeason = episodeDao.countBySeasonId(episode.seasonId)
|
||||||
|
if (remainingEpisodesInSeason == 0) {
|
||||||
|
seasonDao.deleteById(episode.seasonId)
|
||||||
|
|
||||||
|
val remainingSeasonsInSeries = seasonDao.countBySeriesId(episode.seriesId)
|
||||||
|
if (remainingSeasonsInSeries == 0) {
|
||||||
|
seriesDao.deleteById(episode.seriesId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getMovies(): List<Movie> {
|
suspend fun getMovies(): List<Movie> {
|
||||||
val movies = movieDao.getAll()
|
val movies = movieDao.getAll()
|
||||||
return movies.map { entity ->
|
return movies.map { entity ->
|
||||||
|
|||||||
@@ -9,17 +9,22 @@ import androidx.media3.exoplayer.offline.Download
|
|||||||
import androidx.media3.exoplayer.offline.DownloadManager
|
import androidx.media3.exoplayer.offline.DownloadManager
|
||||||
import androidx.media3.exoplayer.offline.DownloadRequest
|
import androidx.media3.exoplayer.offline.DownloadRequest
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import hu.bbara.purefin.core.data.InMemoryMediaRepository
|
||||||
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
import hu.bbara.purefin.core.data.client.JellyfinApiClient
|
||||||
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
import hu.bbara.purefin.core.data.image.JellyfinImageHelper
|
||||||
import hu.bbara.purefin.core.data.room.dao.MovieDao
|
import hu.bbara.purefin.core.data.room.dao.MovieDao
|
||||||
import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource
|
import hu.bbara.purefin.core.data.room.offline.OfflineRoomMediaLocalDataSource
|
||||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||||
|
import hu.bbara.purefin.core.model.Episode
|
||||||
import hu.bbara.purefin.core.model.Movie
|
import hu.bbara.purefin.core.model.Movie
|
||||||
|
import hu.bbara.purefin.core.model.Season
|
||||||
|
import hu.bbara.purefin.core.model.Series
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
@@ -34,7 +39,8 @@ class MediaDownloadManager @Inject constructor(
|
|||||||
private val jellyfinApiClient: JellyfinApiClient,
|
private val jellyfinApiClient: JellyfinApiClient,
|
||||||
private val offlineDataSource: OfflineRoomMediaLocalDataSource,
|
private val offlineDataSource: OfflineRoomMediaLocalDataSource,
|
||||||
private val movieDao: MovieDao,
|
private val movieDao: MovieDao,
|
||||||
private val userSessionRepository: UserSessionRepository
|
private val userSessionRepository: UserSessionRepository,
|
||||||
|
private val inMemoryMediaRepository: InMemoryMediaRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val stateFlows = ConcurrentHashMap<String, MutableStateFlow<DownloadState>>()
|
private val stateFlows = ConcurrentHashMap<String, MutableStateFlow<DownloadState>>()
|
||||||
@@ -143,6 +149,68 @@ class MediaDownloadManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun downloadEpisode(episodeId: UUID) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||||
|
val sources = jellyfinApiClient.getMediaSources(episodeId)
|
||||||
|
val source = sources.firstOrNull() ?: run {
|
||||||
|
Log.e(TAG, "No media sources for episode $episodeId")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = jellyfinApiClient.getMediaPlaybackUrl(episodeId, source) ?: run {
|
||||||
|
Log.e(TAG, "No playback URL for episode $episodeId")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
val episode = jellyfinApiClient.getItemInfo(episodeId)?.toEpisode(serverUrl) ?: run {
|
||||||
|
Log.e(TAG, "Episode not found $episodeId")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
val series = jellyfinApiClient.getItemInfo(episode.seriesId)?.toSeries(serverUrl) ?: run {
|
||||||
|
Log.e(TAG, "Series not found ${episode.seriesId}")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
val season = jellyfinApiClient.getItemInfo(episode.seasonId)?.toSeason(series.id) ?: run {
|
||||||
|
Log.e(TAG, "Season not found ${episode.seasonId}")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offlineDataSource.getSeriesBasic(series.id) == null) {
|
||||||
|
offlineDataSource.saveSeries(listOf(series))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offlineDataSource.getSeason(series.id, season.id) == null) {
|
||||||
|
offlineDataSource.saveSeason(season)
|
||||||
|
}
|
||||||
|
|
||||||
|
offlineDataSource.saveEpisode(episode)
|
||||||
|
|
||||||
|
Log.d(TAG, "Starting download for episode '${episode.title}' from: $url")
|
||||||
|
val request = DownloadRequest.Builder(episodeId.toString(), url.toUri()).build()
|
||||||
|
PurefinDownloadService.sendAddDownload(context, request)
|
||||||
|
Log.d(TAG, "Download request sent for episode $episodeId")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to start download for episode $episodeId", e)
|
||||||
|
getOrCreateStateFlow(episodeId.toString()).value = DownloadState.Failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun cancelEpisodeDownload(episodeId: UUID) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
PurefinDownloadService.sendRemoveDownload(context, episodeId.toString())
|
||||||
|
try {
|
||||||
|
offlineDataSource.deleteEpisodeAndCleanup(episodeId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to remove episode from offline DB", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getOrCreateStateFlow(contentId: String): MutableStateFlow<DownloadState> {
|
private fun getOrCreateStateFlow(contentId: String): MutableStateFlow<DownloadState> {
|
||||||
return stateFlows.getOrPut(contentId) { MutableStateFlow(DownloadState.NotDownloaded) }
|
return stateFlows.getOrPut(contentId) { MutableStateFlow(DownloadState.NotDownloaded) }
|
||||||
}
|
}
|
||||||
@@ -165,6 +233,60 @@ class MediaDownloadManager @Inject constructor(
|
|||||||
return if (hours > 0) "${hours}h ${minutes}m" else "${minutes}m"
|
return if (hours > 0) "${hours}h ${minutes}m" else "${minutes}m"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
|
||||||
|
return Episode(
|
||||||
|
id = id,
|
||||||
|
seriesId = seriesId!!,
|
||||||
|
seasonId = parentId!!,
|
||||||
|
title = name ?: "Unknown title",
|
||||||
|
index = indexNumber ?: 0,
|
||||||
|
releaseDate = productionYear?.toString() ?: "—",
|
||||||
|
rating = officialRating ?: "NR",
|
||||||
|
runtime = formatRuntime(runTimeTicks),
|
||||||
|
progress = userData?.playedPercentage,
|
||||||
|
watched = userData?.played ?: false,
|
||||||
|
format = container?.uppercase() ?: "VIDEO",
|
||||||
|
synopsis = overview ?: "No synopsis available.",
|
||||||
|
heroImageUrl = JellyfinImageHelper.toImageUrl(
|
||||||
|
url = serverUrl,
|
||||||
|
itemId = id,
|
||||||
|
type = ImageType.PRIMARY
|
||||||
|
),
|
||||||
|
cast = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BaseItemDto.toSeries(serverUrl: String): Series {
|
||||||
|
return Series(
|
||||||
|
id = id,
|
||||||
|
libraryId = parentId ?: UUID.randomUUID(),
|
||||||
|
name = name ?: "Unknown",
|
||||||
|
synopsis = overview ?: "No synopsis available",
|
||||||
|
year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
|
||||||
|
heroImageUrl = JellyfinImageHelper.toImageUrl(
|
||||||
|
url = serverUrl,
|
||||||
|
itemId = id,
|
||||||
|
type = ImageType.PRIMARY
|
||||||
|
),
|
||||||
|
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
|
||||||
|
seasonCount = childCount ?: 0,
|
||||||
|
seasons = emptyList(),
|
||||||
|
cast = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BaseItemDto.toSeason(seriesId: UUID): Season {
|
||||||
|
return Season(
|
||||||
|
id = id,
|
||||||
|
seriesId = this.seriesId ?: seriesId,
|
||||||
|
name = name ?: "Unknown",
|
||||||
|
index = indexNumber ?: 0,
|
||||||
|
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
|
||||||
|
episodeCount = childCount ?: 0,
|
||||||
|
episodes = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MediaDownloadManager"
|
private const val TAG = "MediaDownloadManager"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,33 +3,41 @@ package hu.bbara.purefin.feature.shared.content.episode
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import hu.bbara.purefin.core.data.MediaRepository
|
import hu.bbara.purefin.core.data.AppContentRepository
|
||||||
import hu.bbara.purefin.core.data.navigation.NavigationManager
|
import hu.bbara.purefin.core.data.navigation.NavigationManager
|
||||||
import hu.bbara.purefin.core.data.navigation.Route
|
import hu.bbara.purefin.core.data.navigation.Route
|
||||||
import hu.bbara.purefin.core.model.Episode
|
import hu.bbara.purefin.core.model.Episode
|
||||||
|
import hu.bbara.purefin.feature.download.DownloadState
|
||||||
|
import hu.bbara.purefin.feature.download.MediaDownloadManager
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class EpisodeScreenViewModel @Inject constructor(
|
class EpisodeScreenViewModel @Inject constructor(
|
||||||
private val mediaRepository: MediaRepository,
|
private val appContentRepository: AppContentRepository,
|
||||||
private val navigationManager: NavigationManager,
|
private val navigationManager: NavigationManager,
|
||||||
|
private val mediaDownloadManager: MediaDownloadManager,
|
||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
|
|
||||||
private val _episodeId = MutableStateFlow<UUID?>(null)
|
private val _episodeId = MutableStateFlow<UUID?>(null)
|
||||||
|
|
||||||
val episode: StateFlow<Episode?> = combine(
|
val episode: StateFlow<Episode?> = combine(
|
||||||
_episodeId,
|
_episodeId,
|
||||||
mediaRepository.episodes
|
appContentRepository.episodes
|
||||||
) { id, episodesMap ->
|
) { id, episodesMap ->
|
||||||
id?.let { episodesMap[it] }
|
id?.let { episodesMap[it] }
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||||
|
|
||||||
|
private val _downloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
|
||||||
|
val downloadState: StateFlow<DownloadState> = _downloadState.asStateFlow()
|
||||||
|
|
||||||
fun onBack() {
|
fun onBack() {
|
||||||
navigationManager.pop()
|
navigationManager.pop()
|
||||||
}
|
}
|
||||||
@@ -41,6 +49,25 @@ class EpisodeScreenViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun selectEpisode(seriesId: UUID, seasonId: UUID, episodeId: UUID) {
|
fun selectEpisode(seriesId: UUID, seasonId: UUID, episodeId: UUID) {
|
||||||
_episodeId.value = episodeId
|
_episodeId.value = episodeId
|
||||||
|
viewModelScope.launch {
|
||||||
|
mediaDownloadManager.observeDownloadState(episodeId.toString()).collect {
|
||||||
|
_downloadState.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDownloadClick() {
|
||||||
|
val episodeId = _episodeId.value ?: return
|
||||||
|
viewModelScope.launch {
|
||||||
|
when (_downloadState.value) {
|
||||||
|
is DownloadState.NotDownloaded, is DownloadState.Failed -> {
|
||||||
|
mediaDownloadManager.downloadEpisode(episodeId)
|
||||||
|
}
|
||||||
|
is DownloadState.Downloading, is DownloadState.Downloaded -> {
|
||||||
|
mediaDownloadManager.cancelEpisodeDownload(episodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user