Loading app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt +82 −16 Original line number Diff line number Diff line Loading @@ -33,6 +33,8 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable Loading Loading @@ -272,13 +274,32 @@ private fun PagingPlayStoreResultList( } val loadState = lazyItems.loadState val errorState = loadState.refresh as? LoadState.Error ?: loadState.prepend as? LoadState.Error ?: loadState.append as? LoadState.Error val isRefreshing = loadState.refresh is LoadState.Loading val refreshState = loadState.refresh val refreshError = refreshState as? LoadState.Error val appendError = loadState.append as? LoadState.Error val prependError = loadState.prepend as? LoadState.Error val isRefreshing = refreshState is LoadState.Loading val isAppending = loadState.append is LoadState.Loading val isError = errorState != null val isEmpty = !isRefreshing && !isError && lazyItems.itemCount == 0 val hasLoadedCurrentQuery = remember(searchVersion) { mutableStateOf(false) } LaunchedEffect(searchVersion, refreshState, lazyItems.itemCount) { if (refreshState is LoadState.NotLoading && lazyItems.itemCount > 0) { hasLoadedCurrentQuery.value = true } if (refreshState is LoadState.Loading && lazyItems.itemCount == 0) { hasLoadedCurrentQuery.value = false } } val initialLoadError = refreshError != null && !hasLoadedCurrentQuery.value val showFooterError = hasLoadedCurrentQuery.value && listOf( refreshError, appendError, prependError ).any { it != null } val isEmpty = !isRefreshing && refreshError == null && appendError == null && prependError == null && lazyItems.itemCount == 0 Box(modifier = modifier) { when { Loading @@ -286,10 +307,11 @@ private fun PagingPlayStoreResultList( SearchShimmerList() } isError -> { initialLoadError -> { SearchErrorState( onRetry = { lazyItems.retry() }, modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize(), fullScreen = true, ) } Loading Loading @@ -351,6 +373,18 @@ private fun PagingPlayStoreResultList( } } } if (showFooterError) { item(key = "error_footer_play_store") { SearchErrorState( onRetry = { lazyItems.retry() }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 12.dp), fullScreen = false, ) } } } } } Loading Loading @@ -391,13 +425,32 @@ private fun PagingSearchResultList( } val loadState = lazyItems.loadState val errorState = loadState.refresh as? LoadState.Error ?: loadState.prepend as? LoadState.Error ?: loadState.append as? LoadState.Error val isRefreshing = loadState.refresh is LoadState.Loading val refreshState = loadState.refresh val refreshError = refreshState as? LoadState.Error val appendError = loadState.append as? LoadState.Error val prependError = loadState.prepend as? LoadState.Error val isRefreshing = refreshState is LoadState.Loading val isAppending = loadState.append is LoadState.Loading val isError = errorState != null val isEmpty = !isRefreshing && !isError && lazyItems.itemCount == 0 val hasLoadedCurrentQuery = remember(searchVersion) { mutableStateOf(false) } LaunchedEffect(searchVersion, refreshState, lazyItems.itemCount) { if (refreshState is LoadState.NotLoading && lazyItems.itemCount > 0) { hasLoadedCurrentQuery.value = true } if (refreshState is LoadState.Loading && lazyItems.itemCount == 0) { hasLoadedCurrentQuery.value = false } } val initialLoadError = refreshError != null && !hasLoadedCurrentQuery.value val showFooterError = hasLoadedCurrentQuery.value && listOf( refreshError, appendError, prependError ).any { it != null } val isEmpty = !isRefreshing && refreshError == null && appendError == null && prependError == null && lazyItems.itemCount == 0 Box(modifier = modifier) { when { Loading @@ -407,10 +460,11 @@ private fun PagingSearchResultList( ) } isError -> { initialLoadError -> { SearchErrorState( onRetry = { lazyItems.retry() }, modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize(), fullScreen = true, ) } Loading Loading @@ -477,6 +531,18 @@ private fun PagingSearchResultList( } } } if (showFooterError) { item(key = "error_footer") { SearchErrorState( onRetry = { lazyItems.retry() }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 12.dp), fullScreen = false, ) } } } } } Loading app/src/main/java/foundation/e/apps/ui/compose/components/search/SearchErrorState.kt +34 −2 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ package foundation.e.apps.ui.compose.components.search import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding Loading @@ -31,22 +32,37 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import foundation.e.apps.R import foundation.e.apps.ui.compose.theme.AppTheme @Composable fun SearchErrorState( onRetry: () -> Unit, modifier: Modifier = Modifier, fullScreen: Boolean = true, ) { val containerModifier = if (fullScreen) { modifier.fillMaxSize() } else { modifier.fillMaxWidth() } val contentPadding = if (fullScreen) { PaddingValues(all = 24.dp) } else { PaddingValues(horizontal = 16.dp, vertical = 12.dp) } Box( modifier = modifier.fillMaxSize(), modifier = containerModifier, contentAlignment = Alignment.Center ) { Column( modifier = Modifier .fillMaxWidth() .padding(24.dp), .padding(contentPadding), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), ) { Loading @@ -61,3 +77,19 @@ fun SearchErrorState( } } } @Preview(showBackground = true) @Composable private fun SearchErrorStateFullScreenPreview() { AppTheme { SearchErrorState(onRetry = {}, fullScreen = true) } } @Preview(showBackground = true) @Composable private fun SearchErrorStateFooterPreview() { AppTheme { SearchErrorState(onRetry = {}, fullScreen = false) } } app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +14 −7 Original line number Diff line number Diff line Loading @@ -113,6 +113,7 @@ class SearchViewModelV2 @Inject constructor( private data class SearchRequest( val query: String, val visibleTabs: List<SearchTabType>, val version: Int, ) private val searchRequests = MutableStateFlow<SearchRequest?>(null) Loading Loading @@ -238,7 +239,9 @@ class SearchViewModelV2 @Inject constructor( val selectedTab = _uiState.value.selectedTab?.takeIf { visibleTabs.contains(it) } ?: visibleTabs.firstOrNull() var nextVersion = _uiState.value.searchVersion + 1 _uiState.update { current -> nextVersion = current.searchVersion + 1 current.copy( query = trimmedQuery, suggestions = emptyList(), Loading @@ -246,14 +249,15 @@ class SearchViewModelV2 @Inject constructor( availableTabs = visibleTabs, selectedTab = selectedTab, hasSubmittedSearch = visibleTabs.isNotEmpty(), searchVersion = current.searchVersion + 1, searchVersion = nextVersion, ) } if (visibleTabs.isNotEmpty()) { searchRequests.value = SearchRequest( query = trimmedQuery, visibleTabs = visibleTabs visibleTabs = visibleTabs, version = nextVersion, ) _scrollPositions.update { emptyMap() } } Loading Loading @@ -284,17 +288,20 @@ class SearchViewModelV2 @Inject constructor( ) } val currentQuery = _uiState.value.query val shouldUpdateRequest = _uiState.value.hasSubmittedSearch && currentQuery.isNotBlank() val currentState = _uiState.value val currentQuery = currentState.query val shouldUpdateRequest = currentState.hasSubmittedSearch && currentQuery.isNotBlank() if (shouldUpdateRequest && visibleTabs.isNotEmpty()) { searchRequests.value = SearchRequest( query = currentQuery, visibleTabs = visibleTabs visibleTabs = visibleTabs, version = currentState.searchVersion, ) } else if (!_uiState.value.hasSubmittedSearch) { } else if (!currentState.hasSubmittedSearch) { searchRequests.value = SearchRequest( query = "", visibleTabs = visibleTabs visibleTabs = visibleTabs, version = currentState.searchVersion, ) } } Loading Loading
app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultsContent.kt +82 −16 Original line number Diff line number Diff line Loading @@ -33,6 +33,8 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable Loading Loading @@ -272,13 +274,32 @@ private fun PagingPlayStoreResultList( } val loadState = lazyItems.loadState val errorState = loadState.refresh as? LoadState.Error ?: loadState.prepend as? LoadState.Error ?: loadState.append as? LoadState.Error val isRefreshing = loadState.refresh is LoadState.Loading val refreshState = loadState.refresh val refreshError = refreshState as? LoadState.Error val appendError = loadState.append as? LoadState.Error val prependError = loadState.prepend as? LoadState.Error val isRefreshing = refreshState is LoadState.Loading val isAppending = loadState.append is LoadState.Loading val isError = errorState != null val isEmpty = !isRefreshing && !isError && lazyItems.itemCount == 0 val hasLoadedCurrentQuery = remember(searchVersion) { mutableStateOf(false) } LaunchedEffect(searchVersion, refreshState, lazyItems.itemCount) { if (refreshState is LoadState.NotLoading && lazyItems.itemCount > 0) { hasLoadedCurrentQuery.value = true } if (refreshState is LoadState.Loading && lazyItems.itemCount == 0) { hasLoadedCurrentQuery.value = false } } val initialLoadError = refreshError != null && !hasLoadedCurrentQuery.value val showFooterError = hasLoadedCurrentQuery.value && listOf( refreshError, appendError, prependError ).any { it != null } val isEmpty = !isRefreshing && refreshError == null && appendError == null && prependError == null && lazyItems.itemCount == 0 Box(modifier = modifier) { when { Loading @@ -286,10 +307,11 @@ private fun PagingPlayStoreResultList( SearchShimmerList() } isError -> { initialLoadError -> { SearchErrorState( onRetry = { lazyItems.retry() }, modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize(), fullScreen = true, ) } Loading Loading @@ -351,6 +373,18 @@ private fun PagingPlayStoreResultList( } } } if (showFooterError) { item(key = "error_footer_play_store") { SearchErrorState( onRetry = { lazyItems.retry() }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 12.dp), fullScreen = false, ) } } } } } Loading Loading @@ -391,13 +425,32 @@ private fun PagingSearchResultList( } val loadState = lazyItems.loadState val errorState = loadState.refresh as? LoadState.Error ?: loadState.prepend as? LoadState.Error ?: loadState.append as? LoadState.Error val isRefreshing = loadState.refresh is LoadState.Loading val refreshState = loadState.refresh val refreshError = refreshState as? LoadState.Error val appendError = loadState.append as? LoadState.Error val prependError = loadState.prepend as? LoadState.Error val isRefreshing = refreshState is LoadState.Loading val isAppending = loadState.append is LoadState.Loading val isError = errorState != null val isEmpty = !isRefreshing && !isError && lazyItems.itemCount == 0 val hasLoadedCurrentQuery = remember(searchVersion) { mutableStateOf(false) } LaunchedEffect(searchVersion, refreshState, lazyItems.itemCount) { if (refreshState is LoadState.NotLoading && lazyItems.itemCount > 0) { hasLoadedCurrentQuery.value = true } if (refreshState is LoadState.Loading && lazyItems.itemCount == 0) { hasLoadedCurrentQuery.value = false } } val initialLoadError = refreshError != null && !hasLoadedCurrentQuery.value val showFooterError = hasLoadedCurrentQuery.value && listOf( refreshError, appendError, prependError ).any { it != null } val isEmpty = !isRefreshing && refreshError == null && appendError == null && prependError == null && lazyItems.itemCount == 0 Box(modifier = modifier) { when { Loading @@ -407,10 +460,11 @@ private fun PagingSearchResultList( ) } isError -> { initialLoadError -> { SearchErrorState( onRetry = { lazyItems.retry() }, modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize(), fullScreen = true, ) } Loading Loading @@ -477,6 +531,18 @@ private fun PagingSearchResultList( } } } if (showFooterError) { item(key = "error_footer") { SearchErrorState( onRetry = { lazyItems.retry() }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 12.dp), fullScreen = false, ) } } } } } Loading
app/src/main/java/foundation/e/apps/ui/compose/components/search/SearchErrorState.kt +34 −2 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ package foundation.e.apps.ui.compose.components.search import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding Loading @@ -31,22 +32,37 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import foundation.e.apps.R import foundation.e.apps.ui.compose.theme.AppTheme @Composable fun SearchErrorState( onRetry: () -> Unit, modifier: Modifier = Modifier, fullScreen: Boolean = true, ) { val containerModifier = if (fullScreen) { modifier.fillMaxSize() } else { modifier.fillMaxWidth() } val contentPadding = if (fullScreen) { PaddingValues(all = 24.dp) } else { PaddingValues(horizontal = 16.dp, vertical = 12.dp) } Box( modifier = modifier.fillMaxSize(), modifier = containerModifier, contentAlignment = Alignment.Center ) { Column( modifier = Modifier .fillMaxWidth() .padding(24.dp), .padding(contentPadding), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), ) { Loading @@ -61,3 +77,19 @@ fun SearchErrorState( } } } @Preview(showBackground = true) @Composable private fun SearchErrorStateFullScreenPreview() { AppTheme { SearchErrorState(onRetry = {}, fullScreen = true) } } @Preview(showBackground = true) @Composable private fun SearchErrorStateFooterPreview() { AppTheme { SearchErrorState(onRetry = {}, fullScreen = false) } }
app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +14 −7 Original line number Diff line number Diff line Loading @@ -113,6 +113,7 @@ class SearchViewModelV2 @Inject constructor( private data class SearchRequest( val query: String, val visibleTabs: List<SearchTabType>, val version: Int, ) private val searchRequests = MutableStateFlow<SearchRequest?>(null) Loading Loading @@ -238,7 +239,9 @@ class SearchViewModelV2 @Inject constructor( val selectedTab = _uiState.value.selectedTab?.takeIf { visibleTabs.contains(it) } ?: visibleTabs.firstOrNull() var nextVersion = _uiState.value.searchVersion + 1 _uiState.update { current -> nextVersion = current.searchVersion + 1 current.copy( query = trimmedQuery, suggestions = emptyList(), Loading @@ -246,14 +249,15 @@ class SearchViewModelV2 @Inject constructor( availableTabs = visibleTabs, selectedTab = selectedTab, hasSubmittedSearch = visibleTabs.isNotEmpty(), searchVersion = current.searchVersion + 1, searchVersion = nextVersion, ) } if (visibleTabs.isNotEmpty()) { searchRequests.value = SearchRequest( query = trimmedQuery, visibleTabs = visibleTabs visibleTabs = visibleTabs, version = nextVersion, ) _scrollPositions.update { emptyMap() } } Loading Loading @@ -284,17 +288,20 @@ class SearchViewModelV2 @Inject constructor( ) } val currentQuery = _uiState.value.query val shouldUpdateRequest = _uiState.value.hasSubmittedSearch && currentQuery.isNotBlank() val currentState = _uiState.value val currentQuery = currentState.query val shouldUpdateRequest = currentState.hasSubmittedSearch && currentQuery.isNotBlank() if (shouldUpdateRequest && visibleTabs.isNotEmpty()) { searchRequests.value = SearchRequest( query = currentQuery, visibleTabs = visibleTabs visibleTabs = visibleTabs, version = currentState.searchVersion, ) } else if (!_uiState.value.hasSubmittedSearch) { } else if (!currentState.hasSubmittedSearch) { searchRequests.value = SearchRequest( query = "", visibleTabs = visibleTabs visibleTabs = visibleTabs, version = currentState.searchVersion, ) } } Loading