mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
refactor: add CompositeMediaRepository to switch between offline/online based on user session
Extract AppContentRepository interface from MediaRepository for home/library-specific features. CompositeMediaRepository delegates to OfflineMediaRepository or AppContentRepository based on UserSessionRepository.isOfflineMode, allowing content ViewModels to use a single MediaRepository interface that automatically switches data source.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Library
|
||||
import hu.bbara.purefin.core.model.Media
|
||||
import hu.bbara.purefin.core.model.MediaRepositoryState
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.UUID
|
||||
|
||||
interface AppContentRepository : MediaRepository {
|
||||
|
||||
val libraries: StateFlow<List<Library>>
|
||||
val state: StateFlow<MediaRepositoryState>
|
||||
val continueWatching: StateFlow<List<Media>>
|
||||
val nextUp: StateFlow<List<Media>>
|
||||
val latestLibraryContent: StateFlow<Map<UUID, List<Media>>>
|
||||
suspend fun ensureReady()
|
||||
suspend fun refreshHomeData()
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
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.Series
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Switches between [OfflineMediaRepository] and [AppContentRepository] based on
|
||||
* [UserSessionRepository.isOfflineMode]. When offline mode is enabled, all reads
|
||||
* and writes go through the offline (downloaded) repository; otherwise the online
|
||||
* repository is used.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Singleton
|
||||
class CompositeMediaRepository @Inject constructor(
|
||||
private val offlineRepository: OfflineMediaRepository,
|
||||
private val onlineRepository: AppContentRepository,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
) : MediaRepository {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private val activeRepository: Flow<MediaRepository> =
|
||||
userSessionRepository.isOfflineMode.flatMapLatest { offline ->
|
||||
kotlinx.coroutines.flow.flowOf(if (offline) offlineRepository else onlineRepository)
|
||||
}
|
||||
|
||||
override val movies: StateFlow<Map<UUID, Movie>> = activeRepository
|
||||
.flatMapLatest { it.movies }
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
override val series: StateFlow<Map<UUID, Series>> = activeRepository
|
||||
.flatMapLatest { it.series }
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
override val episodes: StateFlow<Map<UUID, Episode>> = activeRepository
|
||||
.flatMapLatest { it.episodes }
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
||||
return activeRepository.flatMapLatest { it.observeSeriesWithContent(seriesId) }
|
||||
}
|
||||
|
||||
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
||||
val isOffline = userSessionRepository.isOfflineMode.stateIn(scope).value
|
||||
val repo = if (isOffline) offlineRepository else onlineRepository
|
||||
repo.updateWatchProgress(mediaId, positionMs, durationMs)
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,11 @@ import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class InMemoryMediaRepository @Inject constructor(
|
||||
class InMemoryAppContentRepository @Inject constructor(
|
||||
val userSessionRepository: UserSessionRepository,
|
||||
val jellyfinApiClient: JellyfinApiClient,
|
||||
private val homeCacheDataStore: DataStore<HomeCache>
|
||||
) : MediaRepository {
|
||||
) : AppContentRepository {
|
||||
|
||||
private val ready = CompletableDeferred<Unit>()
|
||||
private val readyMutex = Mutex()
|
||||
@@ -1,9 +1,6 @@
|
||||
package hu.bbara.purefin.core.data
|
||||
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Library
|
||||
import hu.bbara.purefin.core.model.Media
|
||||
import hu.bbara.purefin.core.model.MediaRepositoryState
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -11,21 +8,9 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.UUID
|
||||
|
||||
interface MediaRepository {
|
||||
|
||||
val libraries: StateFlow<List<Library>>
|
||||
val movies: StateFlow<Map<UUID, Movie>>
|
||||
val series: StateFlow<Map<UUID, Series>>
|
||||
val episodes: StateFlow<Map<UUID, Episode>>
|
||||
val state: StateFlow<MediaRepositoryState>
|
||||
|
||||
val continueWatching: StateFlow<List<Media>>
|
||||
val nextUp: StateFlow<List<Media>>
|
||||
val latestLibraryContent: StateFlow<Map<UUID, List<Media>>>
|
||||
|
||||
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?>
|
||||
|
||||
suspend fun ensureReady()
|
||||
|
||||
suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long)
|
||||
suspend fun refreshHomeData()
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import dagger.hilt.components.SingletonComponent
|
||||
abstract class MediaRepositoryModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindInMemoryMediaRepository(impl: InMemoryMediaRepository): MediaRepository
|
||||
abstract fun bindAppContentRepository(impl: InMemoryAppContentRepository): AppContentRepository
|
||||
|
||||
@Binds
|
||||
abstract fun bindMediaRepository(impl: CompositeMediaRepository): MediaRepository
|
||||
|
||||
}
|
||||
|
||||
@@ -22,23 +22,23 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class OfflineMediaRepository @Inject constructor(
|
||||
private val localDataSource: OfflineRoomMediaLocalDataSource
|
||||
) {
|
||||
) : MediaRepository {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
val movies: StateFlow<Map<UUID, Movie>> = localDataSource.moviesFlow
|
||||
override val movies: StateFlow<Map<UUID, Movie>> = localDataSource.moviesFlow
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
val series: StateFlow<Map<UUID, Series>> = localDataSource.seriesFlow
|
||||
override val series: StateFlow<Map<UUID, Series>> = localDataSource.seriesFlow
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
val episodes: StateFlow<Map<UUID, Episode>> = localDataSource.episodesFlow
|
||||
override val episodes: StateFlow<Map<UUID, Episode>> = localDataSource.episodesFlow
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
||||
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
||||
return localDataSource.observeSeriesWithContent(seriesId)
|
||||
}
|
||||
|
||||
suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
||||
override suspend fun updateWatchProgress(mediaId: UUID, positionMs: Long, durationMs: Long) {
|
||||
if (durationMs <= 0) return
|
||||
val progressPercent = (positionMs.toDouble() / durationMs.toDouble()) * 100.0
|
||||
val watched = progressPercent >= 90.0
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package hu.bbara.purefin.core.data.domain.usecase
|
||||
|
||||
import hu.bbara.purefin.core.data.MediaRepository
|
||||
import hu.bbara.purefin.core.data.AppContentRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class RefreshHomeDataUseCase @Inject constructor(
|
||||
private val repository: MediaRepository
|
||||
private val repository: AppContentRepository
|
||||
) {
|
||||
suspend operator fun invoke() {
|
||||
repository.refreshHomeData()
|
||||
|
||||
@@ -12,7 +12,6 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -31,10 +30,6 @@ class EpisodeScreenViewModel @Inject constructor(
|
||||
id?.let { episodesMap[it] }
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
init {
|
||||
viewModelScope.launch { mediaRepository.ensureReady() }
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
navigationManager.pop()
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -34,20 +33,14 @@ class SeriesViewModel @Inject constructor(
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
init {
|
||||
viewModelScope.launch { mediaRepository.ensureReady() }
|
||||
}
|
||||
|
||||
fun onSelectEpisode(seriesId: UUID, seasonId:UUID, episodeId: UUID) {
|
||||
viewModelScope.launch {
|
||||
navigationManager.navigate(Route.EpisodeRoute(
|
||||
EpisodeDto(
|
||||
id = episodeId,
|
||||
seasonId = seasonId,
|
||||
seriesId = seriesId
|
||||
)
|
||||
))
|
||||
}
|
||||
fun onSelectEpisode(seriesId: UUID, seasonId: UUID, episodeId: UUID) {
|
||||
navigationManager.navigate(Route.EpisodeRoute(
|
||||
EpisodeDto(
|
||||
id = episodeId,
|
||||
seasonId = seasonId,
|
||||
seriesId = seriesId
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
|
||||
@@ -3,7 +3,7 @@ package hu.bbara.purefin.feature.shared.home
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.domain.usecase.RefreshHomeDataUseCase
|
||||
import hu.bbara.purefin.core.data.navigation.EpisodeDto
|
||||
import hu.bbara.purefin.core.data.navigation.LibraryDto
|
||||
@@ -25,7 +25,7 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomePageViewModel @Inject constructor(
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val appContentRepository: AppContentRepository,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
private val navigationManager: NavigationManager,
|
||||
private val refreshHomeDataUseCase: RefreshHomeDataUseCase
|
||||
@@ -37,7 +37,7 @@ class HomePageViewModel @Inject constructor(
|
||||
initialValue = ""
|
||||
)
|
||||
|
||||
val libraries = mediaRepository.libraries.map { libraries ->
|
||||
val libraries = appContentRepository.libraries.map { libraries ->
|
||||
libraries.map {
|
||||
LibraryItem(
|
||||
id = it.id,
|
||||
@@ -45,8 +45,8 @@ class HomePageViewModel @Inject constructor(
|
||||
type = it.type,
|
||||
posterUrl = it.posterUrl,
|
||||
isEmpty = when(it.type) {
|
||||
CollectionType.MOVIES -> mediaRepository.movies.value.isEmpty()
|
||||
CollectionType.TVSHOWS -> mediaRepository.series.value.isEmpty()
|
||||
CollectionType.MOVIES -> appContentRepository.movies.value.isEmpty()
|
||||
CollectionType.TVSHOWS -> appContentRepository.series.value.isEmpty()
|
||||
else -> true
|
||||
}
|
||||
)
|
||||
@@ -60,9 +60,9 @@ class HomePageViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
val continueWatching = combine(
|
||||
mediaRepository.continueWatching,
|
||||
mediaRepository.movies,
|
||||
mediaRepository.episodes
|
||||
appContentRepository.continueWatching,
|
||||
appContentRepository.movies,
|
||||
appContentRepository.episodes
|
||||
) { list, moviesMap, episodesMap ->
|
||||
list.mapNotNull { media ->
|
||||
when (media) {
|
||||
@@ -82,8 +82,8 @@ class HomePageViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
val nextUp = combine(
|
||||
mediaRepository.nextUp,
|
||||
mediaRepository.episodes
|
||||
appContentRepository.nextUp,
|
||||
appContentRepository.episodes
|
||||
) { list, episodesMap ->
|
||||
list.mapNotNull { media ->
|
||||
when (media) {
|
||||
@@ -100,10 +100,10 @@ class HomePageViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
val latestLibraryContent = combine(
|
||||
mediaRepository.latestLibraryContent,
|
||||
mediaRepository.movies,
|
||||
mediaRepository.series,
|
||||
mediaRepository.episodes
|
||||
appContentRepository.latestLibraryContent,
|
||||
appContentRepository.movies,
|
||||
appContentRepository.series,
|
||||
appContentRepository.episodes
|
||||
) { libraryMap, moviesMap, seriesMap, episodesMap ->
|
||||
libraryMap.mapValues { (_, items) ->
|
||||
items.mapNotNull { media ->
|
||||
@@ -131,7 +131,7 @@ class HomePageViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
init {
|
||||
viewModelScope.launch { mediaRepository.ensureReady() }
|
||||
viewModelScope.launch { appContentRepository.ensureReady() }
|
||||
}
|
||||
|
||||
fun onLibrarySelected(id: UUID, name: String) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.feature.shared.home.PosterItem
|
||||
import hu.bbara.purefin.core.data.MediaRepository
|
||||
import hu.bbara.purefin.core.data.AppContentRepository
|
||||
import hu.bbara.purefin.core.data.navigation.MovieDto
|
||||
import hu.bbara.purefin.core.data.navigation.NavigationManager
|
||||
import hu.bbara.purefin.core.data.navigation.Route
|
||||
@@ -22,13 +22,13 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LibraryViewModel @Inject constructor(
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val appContentRepository: AppContentRepository,
|
||||
private val navigationManager: NavigationManager
|
||||
) : ViewModel() {
|
||||
|
||||
private val selectedLibrary = MutableStateFlow<UUID?>(null)
|
||||
|
||||
val contents: StateFlow<List<PosterItem>> = combine(selectedLibrary, mediaRepository.libraries) {
|
||||
val contents: StateFlow<List<PosterItem>> = combine(selectedLibrary, appContentRepository.libraries) {
|
||||
libraryId, libraries ->
|
||||
if (libraryId == null) {
|
||||
return@combine emptyList()
|
||||
@@ -46,7 +46,7 @@ class LibraryViewModel @Inject constructor(
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
|
||||
init {
|
||||
viewModelScope.launch { mediaRepository.ensureReady() }
|
||||
viewModelScope.launch { appContentRepository.ensureReady() }
|
||||
}
|
||||
|
||||
fun onMovieSelected(movieId: UUID) {
|
||||
|
||||
Reference in New Issue
Block a user