From 9f8292b04d8146d3775b9f519d7bd3a41c5cda89 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Thu, 4 Jun 2026 19:26:47 +0000 Subject: [PATCH] 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. --- .../core/download/MediaDownloadManager.kt | 3 + .../core/feature/browse/home/AppViewModel.kt | 29 ++-------- .../home/refresh/HomeRefreshCoordinator.kt | 57 +++++++++++++++++++ .../home/refresh/HomeRefreshSideEffect.kt | 5 ++ .../refresh/HomeRefreshSideEffectModule.kt | 18 ++++++ ...SyncSmartDownloadsHomeRefreshSideEffect.kt | 13 +++++ 6 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshCoordinator.kt create mode 100644 core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshSideEffect.kt create mode 100644 core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshSideEffectModule.kt create mode 100644 core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/SyncSmartDownloadsHomeRefreshSideEffect.kt diff --git a/core/src/main/java/hu/bbara/purefin/core/download/MediaDownloadManager.kt b/core/src/main/java/hu/bbara/purefin/core/download/MediaDownloadManager.kt index 9e472252..3c52a31e 100644 --- a/core/src/main/java/hu/bbara/purefin/core/download/MediaDownloadManager.kt +++ b/core/src/main/java/hu/bbara/purefin/core/download/MediaDownloadManager.kt @@ -12,6 +12,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import hu.bbara.purefin.core.data.DownloadMediaSourceResolver import hu.bbara.purefin.core.data.OfflineCatalogStore import hu.bbara.purefin.core.data.SmartDownloadStore +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay @@ -230,6 +231,8 @@ class MediaDownloadManager @Inject constructor( for (seriesId in enabledSeriesIds) { try { syncSmartDownloadsForSeries(seriesId) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Timber.tag(TAG).e(e, "Smart download sync failed for series $seriesId") } diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/AppViewModel.kt b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/AppViewModel.kt index e73761e0..c8006c9b 100644 --- a/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/AppViewModel.kt +++ b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/AppViewModel.kt @@ -7,7 +7,7 @@ import hu.bbara.purefin.core.data.HomeRepository import hu.bbara.purefin.core.data.LocalMediaRepository import hu.bbara.purefin.core.data.NetworkMonitor 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.settings.SettingsRepository import hu.bbara.purefin.core.model.EpisodeUiModel @@ -41,7 +41,7 @@ class AppViewModel @Inject constructor( private val userSessionRepository: UserSessionRepository, private val jellyfinMediaMetadataUpdater: JellyfinMediaMetadataUpdater, private val navigationManager: NavigationManager, - private val mediaDownloadManager: MediaDownloadController, + private val homeRefreshCoordinator: HomeRefreshCoordinator, private val settingsRepository: SettingsRepository, networkMonitor: NetworkMonitor, ) : ViewModel() { @@ -253,35 +253,16 @@ class AppViewModel @Inject constructor( fun onResumed() { viewModelScope.launch { - try { - homeRepository.refreshHomeData() - } catch (e: Exception) { - // Refresh is best-effort; don't crash on failure - } - } - viewModelScope.launch { - try { - mediaDownloadManager.syncSmartDownloads() - } catch (_: Exception) { } + homeRefreshCoordinator.onResumed() } } fun onRefresh() { viewModelScope.launch { - _isRefreshing.value = true - try { - homeRepository.refreshHomeData() - } catch (e: Exception) { - // Refresh is best-effort; don't crash on failure - } finally { - _isRefreshing.value = false + homeRefreshCoordinator.onRefresh { isRefreshing -> + _isRefreshing.value = isRefreshing } } - viewModelScope.launch { - try { - mediaDownloadManager.syncSmartDownloads() - } catch (_: Exception) { } - } } fun openSearch() { diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshCoordinator.kt b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshCoordinator.kt new file mode 100644 index 00000000..30b44cdd --- /dev/null +++ b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshCoordinator.kt @@ -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) { } + } + } + } + } + } +} diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshSideEffect.kt b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshSideEffect.kt new file mode 100644 index 00000000..b0c3c738 --- /dev/null +++ b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshSideEffect.kt @@ -0,0 +1,5 @@ +package hu.bbara.purefin.core.feature.browse.home.refresh + +interface HomeRefreshSideEffect { + suspend fun run() +} diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshSideEffectModule.kt b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshSideEffectModule.kt new file mode 100644 index 00000000..102bfd0e --- /dev/null +++ b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/HomeRefreshSideEffectModule.kt @@ -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 +} diff --git a/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/SyncSmartDownloadsHomeRefreshSideEffect.kt b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/SyncSmartDownloadsHomeRefreshSideEffect.kt new file mode 100644 index 00000000..33450892 --- /dev/null +++ b/core/src/main/java/hu/bbara/purefin/core/feature/browse/home/refresh/SyncSmartDownloadsHomeRefreshSideEffect.kt @@ -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() + } +}