refactor(home): extract refresh side effects

Centralize home refresh orchestration so future refresh side effects can be added through Hilt multibindings instead of expanding AppViewModel.
This commit is contained in:
2026-06-04 19:26:47 +00:00
parent fa87005d01
commit 9f8292b04d
6 changed files with 101 additions and 24 deletions

View File

@@ -12,6 +12,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import hu.bbara.purefin.core.data.DownloadMediaSourceResolver import hu.bbara.purefin.core.data.DownloadMediaSourceResolver
import hu.bbara.purefin.core.data.OfflineCatalogStore import hu.bbara.purefin.core.data.OfflineCatalogStore
import hu.bbara.purefin.core.data.SmartDownloadStore import hu.bbara.purefin.core.data.SmartDownloadStore
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -230,6 +231,8 @@ class MediaDownloadManager @Inject constructor(
for (seriesId in enabledSeriesIds) { for (seriesId in enabledSeriesIds) {
try { try {
syncSmartDownloadsForSeries(seriesId) syncSmartDownloadsForSeries(seriesId)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) { } catch (e: Exception) {
Timber.tag(TAG).e(e, "Smart download sync failed for series $seriesId") Timber.tag(TAG).e(e, "Smart download sync failed for series $seriesId")
} }

View File

@@ -7,7 +7,7 @@ import hu.bbara.purefin.core.data.HomeRepository
import hu.bbara.purefin.core.data.LocalMediaRepository import hu.bbara.purefin.core.data.LocalMediaRepository
import hu.bbara.purefin.core.data.NetworkMonitor import hu.bbara.purefin.core.data.NetworkMonitor
import hu.bbara.purefin.core.data.UserSessionRepository import hu.bbara.purefin.core.data.UserSessionRepository
import hu.bbara.purefin.core.download.MediaDownloadController import hu.bbara.purefin.core.feature.browse.home.refresh.HomeRefreshCoordinator
import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater import hu.bbara.purefin.core.jellyfin.JellyfinMediaMetadataUpdater
import hu.bbara.purefin.core.settings.SettingsRepository import hu.bbara.purefin.core.settings.SettingsRepository
import hu.bbara.purefin.core.model.EpisodeUiModel import hu.bbara.purefin.core.model.EpisodeUiModel
@@ -41,7 +41,7 @@ class AppViewModel @Inject constructor(
private val userSessionRepository: UserSessionRepository, private val userSessionRepository: UserSessionRepository,
private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater, private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater,
private val navigationManager: NavigationManager, private val navigationManager: NavigationManager,
private val mediaDownloadManager: MediaDownloadController, private val homeRefreshCoordinator: HomeRefreshCoordinator,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
) : ViewModel() { ) : ViewModel() {
@@ -253,35 +253,16 @@ class AppViewModel @Inject constructor(
fun onResumed() { fun onResumed() {
viewModelScope.launch { viewModelScope.launch {
try { homeRefreshCoordinator.onResumed()
homeRepository.refreshHomeData()
} catch (e: Exception) {
// Refresh is best-effort; don't crash on failure
}
}
viewModelScope.launch {
try {
mediaDownloadManager.syncSmartDownloads()
} catch (_: Exception) { }
} }
} }
fun onRefresh() { fun onRefresh() {
viewModelScope.launch { viewModelScope.launch {
_isRefreshing.value = true homeRefreshCoordinator.onRefresh { isRefreshing ->
try { _isRefreshing.value = isRefreshing
homeRepository.refreshHomeData()
} catch (e: Exception) {
// Refresh is best-effort; don't crash on failure
} finally {
_isRefreshing.value = false
} }
} }
viewModelScope.launch {
try {
mediaDownloadManager.syncSmartDownloads()
} catch (_: Exception) { }
}
} }
fun openSearch() { fun openSearch() {

View File

@@ -0,0 +1,57 @@
package hu.bbara.purefin.core.feature.browse.home.refresh
import hu.bbara.purefin.core.data.HomeRepository
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
class HomeRefreshCoordinator @Inject constructor(
private val homeRepository: HomeRepository,
private val sideEffects: Set<@JvmSuppressWildcards HomeRefreshSideEffect>,
) {
private val sideEffectsMutex = Mutex()
suspend fun onResumed() {
refreshHomeData()
runSideEffects()
}
suspend fun onRefresh(setRefreshing: (Boolean) -> Unit) {
refreshHomeData(setRefreshing)
runSideEffects()
}
private suspend fun refreshHomeData(
setRefreshing: ((Boolean) -> Unit)? = null,
) {
setRefreshing?.invoke(true)
try {
homeRepository.refreshHomeData()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
// Refresh is best-effort; don't crash on failure
} finally {
setRefreshing?.invoke(false)
}
}
private suspend fun runSideEffects() {
sideEffectsMutex.withLock {
coroutineScope {
sideEffects.forEach { sideEffect ->
launch {
try {
sideEffect.run()
} catch (e: CancellationException) {
throw e
} catch (_: Exception) { }
}
}
}
}
}
}

View File

@@ -0,0 +1,5 @@
package hu.bbara.purefin.core.feature.browse.home.refresh
interface HomeRefreshSideEffect {
suspend fun run()
}

View File

@@ -0,0 +1,18 @@
package hu.bbara.purefin.core.feature.browse.home.refresh
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
@Module
@InstallIn(SingletonComponent::class)
abstract class HomeRefreshSideEffectModule {
@Binds
@IntoSet
abstract fun bindSyncSmartDownloadsHomeRefreshSideEffect(
impl: SyncSmartDownloadsHomeRefreshSideEffect
): HomeRefreshSideEffect
}

View File

@@ -0,0 +1,13 @@
package hu.bbara.purefin.core.feature.browse.home.refresh
import hu.bbara.purefin.core.download.MediaDownloadController
import javax.inject.Inject
class SyncSmartDownloadsHomeRefreshSideEffect @Inject constructor(
private val mediaDownloadController: MediaDownloadController,
) : HomeRefreshSideEffect {
override suspend fun run() {
mediaDownloadController.syncSmartDownloads()
}
}