From 5ca127434dcabd0225ad84be000025d796cca825 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Tue, 3 Mar 2026 21:18:50 +0100 Subject: [PATCH] feat: add pull-to-refresh gesture to HomeScreen Wrap HomeContent with PullToRefreshBox to allow refreshing library, continue watching, and next up sections by pulling down. Also fix refreshHomeData to suspend until loading completes so the refresh indicator dismisses properly. --- .../hu/bbara/purefin/app/home/HomePage.kt | 3 +++ .../bbara/purefin/app/home/ui/HomeContent.kt | 13 ++++++++++++- .../core/data/InMemoryAppContentRepository.kt | 5 ++++- .../feature/shared/home/HomePageViewModel.kt | 19 +++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt b/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt index bd2950e..4fed9b3 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt @@ -47,6 +47,7 @@ fun HomePage( val continueWatching = viewModel.continueWatching.collectAsState() val nextUp = viewModel.nextUp.collectAsState() val latestLibraryContent = viewModel.latestLibraryContent.collectAsState() + val isRefreshing = viewModel.isRefreshing.collectAsState() LifecycleResumeEffect(Unit) { viewModel.onResumed() @@ -89,6 +90,8 @@ fun HomePage( libraryContent = latestLibraryContent.value, continueWatching = continueWatching.value, nextUp = nextUp.value, + isRefreshing = isRefreshing.value, + onRefresh = viewModel::onRefresh, onMovieSelected = viewModel::onMovieSelected, onSeriesSelected = viewModel::onSeriesSelected, onEpisodeSelected = viewModel::onEpisodeSelected, diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt index 81413cf..f66b952 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt @@ -6,7 +6,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -16,21 +18,29 @@ import hu.bbara.purefin.feature.shared.home.NextUpItem import hu.bbara.purefin.feature.shared.home.PosterItem import org.jellyfin.sdk.model.UUID +@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeContent( libraries: List, libraryContent: Map>, continueWatching: List, nextUp: List, + isRefreshing: Boolean, + onRefresh: () -> Unit, onMovieSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, modifier: Modifier = Modifier ) { - LazyColumn( + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, modifier = modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) + ) { + LazyColumn( + modifier = Modifier.fillMaxSize() ) { item { Spacer(modifier = Modifier.height(8.dp)) @@ -69,4 +79,5 @@ fun HomeContent( Spacer(modifier = Modifier.height(8.dp)) } } + } } diff --git a/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryAppContentRepository.kt b/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryAppContentRepository.kt index 07f6b76..3a07634 100644 --- a/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryAppContentRepository.kt +++ b/core/data/src/main/java/hu/bbara/purefin/core/data/InMemoryAppContentRepository.kt @@ -297,15 +297,18 @@ class InMemoryAppContentRepository @Inject constructor( if (!isOnline) return if(loadJob?.isActive == true) { + loadJob?.join() return } - loadJob = scope.launch { + val job = scope.launch { loadLibraries() loadContinueWatching() loadNextUp() loadLatestLibraryContent() persistHomeCache() } + loadJob = job + job.join() } private suspend fun serverUrl(): String { diff --git a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomePageViewModel.kt b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomePageViewModel.kt index 948645f..51b8043 100644 --- a/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomePageViewModel.kt +++ b/feature/shared/src/main/java/hu/bbara/purefin/feature/shared/home/HomePageViewModel.kt @@ -13,7 +13,10 @@ import hu.bbara.purefin.core.data.navigation.Route import hu.bbara.purefin.core.data.navigation.SeriesDto import hu.bbara.purefin.core.data.session.UserSessionRepository import hu.bbara.purefin.core.model.Media +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.map import kotlinx.coroutines.flow.stateIn @@ -31,6 +34,9 @@ class HomePageViewModel @Inject constructor( private val refreshHomeDataUseCase: RefreshHomeDataUseCase ) : ViewModel() { + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow = _isRefreshing.asStateFlow() + private val _url = userSessionRepository.serverUrl.stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, @@ -195,6 +201,19 @@ class HomePageViewModel @Inject constructor( } } + fun onRefresh() { + viewModelScope.launch { + _isRefreshing.value = true + try { + refreshHomeDataUseCase() + } catch (e: Exception) { + // Refresh is best-effort; don't crash on failure + } finally { + _isRefreshing.value = false + } + } + } + fun logout() { viewModelScope.launch { userSessionRepository.setLoggedIn(false)