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.core.navigation.SeriesDto
import hu.bbara.purefin.model.Episode import hu.bbara.purefin.model.Episode
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.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.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -30,19 +33,23 @@ class EpisodeScreenViewModel @Inject constructor(
private val _episodeId = MutableStateFlow<UUID?>(null) private val _episodeId = MutableStateFlow<UUID?>(null)
private val _seriesId = MutableStateFlow<UUID?>(null) private val _seriesId = MutableStateFlow<UUID?>(null)
val episode: StateFlow<Episode?> = combine( @OptIn(ExperimentalCoroutinesApi::class)
_episodeId, val episode: StateFlow<Episode?> = _episodeId
mediaCatalogReader.episodes .flatMapLatest { episodeId ->
) { id, episodesMap -> if (episodeId == null) flowOf(null) else mediaCatalogReader.getEpisode(episodeId)
id?.let { episodesMap[it] } }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
val seriesTitle: StateFlow<String?> = combine( @OptIn(ExperimentalCoroutinesApi::class)
_seriesId, val seriesTitle: StateFlow<String?> = _seriesId
mediaCatalogReader.series .flatMapLatest { seriesId ->
) { seriesId, seriesMap -> if (seriesId == null) {
seriesId?.let { id -> seriesMap[id]?.name } flowOf(null)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) } else {
mediaCatalogReader.getSeries(seriesId).map { it?.name }
}
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
private val _downloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded) private val _downloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
val downloadState: StateFlow<DownloadState> = _downloadState.asStateFlow() 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.core.navigation.Route
import hu.bbara.purefin.model.Movie import hu.bbara.purefin.model.Movie
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -28,12 +30,12 @@ class MovieScreenViewModel @Inject constructor(
private val _movieId = MutableStateFlow<UUID?>(null) private val _movieId = MutableStateFlow<UUID?>(null)
val movie: StateFlow<Movie?> = combine( @OptIn(ExperimentalCoroutinesApi::class)
_movieId, val movie: StateFlow<Movie?> = _movieId
mediaCatalogReader.movies .flatMapLatest { movieId ->
) { movieId, movies -> if (movieId == null) flowOf(null) else mediaCatalogReader.getMovie(movieId)
movieId?.let { movies[it] } }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
private val _downloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded) private val _downloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
val downloadState: StateFlow<DownloadState> = _downloadState.asStateFlow() 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.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
@@ -33,12 +35,11 @@ class SeriesViewModel @Inject constructor(
private val _seriesId = MutableStateFlow<UUID?>(null) private val _seriesId = MutableStateFlow<UUID?>(null)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val series: StateFlow<Series?> = combine( val series: StateFlow<Series?> = _seriesId
_seriesId, .flatMapLatest { seriesId ->
mediaCatalogReader.series if (seriesId == null) flowOf(null) else mediaCatalogReader.getSeries(seriesId)
) { seriesId, seriesMap -> }
seriesId?.let { seriesMap[it] } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
private val _seriesDownloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded) private val _seriesDownloadState = MutableStateFlow<DownloadState>(DownloadState.NotDownloaded)
val seriesDownloadState: StateFlow<DownloadState> = _seriesDownloadState 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -58,7 +59,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
moviesState.update { current -> current + (movie.id to movie) } 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?> { override suspend fun getSeries(id: UUID): Flow<Series?> {
@@ -72,7 +73,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
seriesState.update { current -> current + (series.id to series) } 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?> { override suspend fun getEpisode(id: UUID): Flow<Episode?> {
@@ -88,7 +89,7 @@ class InMemoryLocalMediaRepository @Inject constructor(
} }
val episode = episodesState.value[id] ?: return flowOf(null) val episode = episodesState.value[id] ?: return flowOf(null)
loadSeasons(seriesId = episode.seriesId) loadSeasons(seriesId = episode.seriesId)
return episodesState.map { it[id] } return episodesState.map { it[id] }.distinctUntilChanged()
} }
fun upsertMovies(movies: List<Movie>) { fun upsertMovies(movies: List<Movie>) {
@@ -162,10 +163,14 @@ class InMemoryLocalMediaRepository @Inject constructor(
return return
} }
if (episodesState.value.containsKey(mediaId)) { if (episodesState.value.containsKey(mediaId)) {
var updatedEpisode: Episode? = null
episodesState.update { current -> episodesState.update { current ->
val episode = current[mediaId] ?: return@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 return
} }
if (episodesState.value.containsKey(mediaId)) { if (episodesState.value.containsKey(mediaId)) {
var updatedEpisode: Episode? = null
episodesState.update { current -> episodesState.update { current ->
val episode = current[mediaId] ?: return@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
} }
} }
} }