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.
This commit is contained in:
2026-03-03 21:18:50 +01:00
parent cc972e0e89
commit 5ca127434d
4 changed files with 38 additions and 2 deletions

View File

@@ -47,6 +47,7 @@ fun HomePage(
val continueWatching = viewModel.continueWatching.collectAsState() val continueWatching = viewModel.continueWatching.collectAsState()
val nextUp = viewModel.nextUp.collectAsState() val nextUp = viewModel.nextUp.collectAsState()
val latestLibraryContent = viewModel.latestLibraryContent.collectAsState() val latestLibraryContent = viewModel.latestLibraryContent.collectAsState()
val isRefreshing = viewModel.isRefreshing.collectAsState()
LifecycleResumeEffect(Unit) { LifecycleResumeEffect(Unit) {
viewModel.onResumed() viewModel.onResumed()
@@ -89,6 +90,8 @@ fun HomePage(
libraryContent = latestLibraryContent.value, libraryContent = latestLibraryContent.value,
continueWatching = continueWatching.value, continueWatching = continueWatching.value,
nextUp = nextUp.value, nextUp = nextUp.value,
isRefreshing = isRefreshing.value,
onRefresh = viewModel::onRefresh,
onMovieSelected = viewModel::onMovieSelected, onMovieSelected = viewModel::onMovieSelected,
onSeriesSelected = viewModel::onSeriesSelected, onSeriesSelected = viewModel::onSeriesSelected,
onEpisodeSelected = viewModel::onEpisodeSelected, onEpisodeSelected = viewModel::onEpisodeSelected,

View File

@@ -6,7 +6,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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 hu.bbara.purefin.feature.shared.home.PosterItem
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomeContent( fun HomeContent(
libraries: List<LibraryItem>, libraries: List<LibraryItem>,
libraryContent: Map<UUID, List<PosterItem>>, libraryContent: Map<UUID, List<PosterItem>>,
continueWatching: List<ContinueWatchingItem>, continueWatching: List<ContinueWatchingItem>,
nextUp: List<NextUpItem>, nextUp: List<NextUpItem>,
isRefreshing: Boolean,
onRefresh: () -> Unit,
onMovieSelected: (UUID) -> Unit, onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LazyColumn( PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = onRefresh,
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) { ) {
item { item {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -69,4 +79,5 @@ fun HomeContent(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
} }
}
} }

View File

@@ -297,15 +297,18 @@ class InMemoryAppContentRepository @Inject constructor(
if (!isOnline) return if (!isOnline) return
if(loadJob?.isActive == true) { if(loadJob?.isActive == true) {
loadJob?.join()
return return
} }
loadJob = scope.launch { val job = scope.launch {
loadLibraries() loadLibraries()
loadContinueWatching() loadContinueWatching()
loadNextUp() loadNextUp()
loadLatestLibraryContent() loadLatestLibraryContent()
persistHomeCache() persistHomeCache()
} }
loadJob = job
job.join()
} }
private suspend fun serverUrl(): String { private suspend fun serverUrl(): String {

View File

@@ -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.navigation.SeriesDto
import hu.bbara.purefin.core.data.session.UserSessionRepository import hu.bbara.purefin.core.data.session.UserSessionRepository
import hu.bbara.purefin.core.model.Media import hu.bbara.purefin.core.model.Media
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
@@ -31,6 +34,9 @@ class HomePageViewModel @Inject constructor(
private val refreshHomeDataUseCase: RefreshHomeDataUseCase private val refreshHomeDataUseCase: RefreshHomeDataUseCase
) : ViewModel() { ) : ViewModel() {
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()
private val _url = userSessionRepository.serverUrl.stateIn( private val _url = userSessionRepository.serverUrl.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.Eagerly, 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() { fun logout() {
viewModelScope.launch { viewModelScope.launch {
userSessionRepository.setLoggedIn(false) userSessionRepository.setLoggedIn(false)