Make media detail flows reactive

This commit is contained in:
2026-05-13 16:43:42 +02:00
parent 406d0f4ed8
commit 3ee4b558f5
4 changed files with 74 additions and 31 deletions

View File

@@ -11,11 +11,14 @@ import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.core.navigation.SeriesDto
import hu.bbara.purefin.model.Episode
import java.util.UUID
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -30,19 +33,23 @@ class EpisodeScreenViewModel @Inject constructor(
private val _episodeId = MutableStateFlow<UUID?>(null)
private val _seriesId = MutableStateFlow<UUID?>(null)
val episode: StateFlow<Episode?> = combine(
_episodeId,
mediaCatalogReader.episodes
) { id, episodesMap ->
id?.let { episodesMap[it] }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
@OptIn(ExperimentalCoroutinesApi::class)
val episode: StateFlow<Episode?> = _episodeId
.flatMapLatest { episodeId ->
if (episodeId == null) flowOf(null) else mediaCatalogReader.getEpisode(episodeId)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
val seriesTitle: StateFlow<String?> = combine(
_seriesId,
mediaCatalogReader.series
) { seriesId, seriesMap ->
seriesId?.let { id -> seriesMap[id]?.name }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
@OptIn(ExperimentalCoroutinesApi::class)
val seriesTitle: StateFlow<String?> = _seriesId
.flatMapLatest { seriesId ->
if (seriesId == null) {
flowOf(null)
} else {
mediaCatalogReader.getSeries(seriesId).map { it?.name }
}
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
private val _downloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
val downloadState: StateFlow<DownloadState> = _downloadState.asStateFlow()

View File

@@ -10,11 +10,13 @@ import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.core.navigation.Route
import hu.bbara.purefin.model.Movie
import java.util.UUID
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -28,12 +30,12 @@ class MovieScreenViewModel @Inject constructor(
private val _movieId = MutableStateFlow<UUID?>(null)
val movie: StateFlow<Movie?> = combine(
_movieId,
mediaCatalogReader.movies
) { movieId, movies ->
movieId?.let { movies[it] }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
@OptIn(ExperimentalCoroutinesApi::class)
val movie: StateFlow<Movie?> = _movieId
.flatMapLatest { movieId ->
if (movieId == null) flowOf(null) else mediaCatalogReader.getMovie(movieId)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
private val _downloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
val downloadState: StateFlow<DownloadState> = _downloadState.asStateFlow()

View File

@@ -18,6 +18,8 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.util.UUID
@@ -33,12 +35,11 @@ class SeriesViewModel @Inject constructor(
private val _seriesId = MutableStateFlow<UUID?>(null)
@OptIn(ExperimentalCoroutinesApi::class)
val series: StateFlow<Series?> = combine(
_seriesId,
mediaCatalogReader.series
) { seriesId, seriesMap ->
seriesId?.let { seriesMap[it] }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
val series: StateFlow<Series?> = _seriesId
.flatMapLatest { seriesId ->
if (seriesId == null) flowOf(null) else mediaCatalogReader.getSeries(seriesId)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
private val _seriesDownloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
val seriesDownloadState: StateFlow<DownloadState> = _seriesDownloadState

View File

@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -58,7 +59,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
moviesState.update { current -> current + (movie.id to movie) }
}
}
return moviesState.map { it[id] }
return moviesState.map { it[id] }.distinctUntilChanged()
}
override suspend fun getSeries(id: UUID): Flow<Series?> {
@@ -72,7 +73,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
seriesState.update { current -> current + (series.id to series) }
}
}
return seriesState.map { it[id] }
return seriesState.map { it[id] }.distinctUntilChanged()
}
override suspend fun getEpisode(id: UUID): Flow<Episode?> {
@@ -88,7 +89,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
}
val episode = episodesState.value[id] ?: return flowOf(null)
loadSeasons(seriesId = episode.seriesId)
return episodesState.map { it[id] }
return episodesState.map { it[id] }.distinctUntilChanged()
}
fun upsertMovies(movies: List<Movie>) {
@@ -162,10 +163,14 @@ class InMemoryLocalMediaRepository @Inject constructor(
return
}
if (episodesState.value.containsKey(mediaId)) {
var updatedEpisode: Episode? = null
episodesState.update { current ->
val episode = current[mediaId] ?: return@update current
current + (mediaId to episode.copy(progress = progressPercent, watched = watched))
val updated = episode.copy(progress = progressPercent, watched = watched)
updatedEpisode = updated
current + (mediaId to updated)
}
updatedEpisode?.let(::updateLoadedSeriesEpisode)
}
}
@@ -178,9 +183,37 @@ class InMemoryLocalMediaRepository @Inject constructor(
return
}
if (episodesState.value.containsKey(mediaId)) {
var updatedEpisode: Episode? = null
episodesState.update { current ->
val episode = current[mediaId] ?: return@update current
current + (mediaId to episode.copy(watched = watched))
val updated = episode.copy(watched = watched)
updatedEpisode = updated
current + (mediaId to updated)
}
updatedEpisode?.let(::updateLoadedSeriesEpisode)
}
}
private fun updateLoadedSeriesEpisode(updatedEpisode: Episode) {
seriesState.update { current ->
val series = current[updatedEpisode.seriesId] ?: return@update current
var changed = false
val seasons = series.seasons.map { season ->
if (season.episodes.none { it.id == updatedEpisode.id }) {
season
} else {
changed = true
season.copy(
episodes = season.episodes.map { episode ->
if (episode.id == updatedEpisode.id) updatedEpisode else episode
}
)
}
}
if (changed) {
current + (series.id to series.copy(seasons = seasons))
} else {
current
}
}
}