Implement LoadStateIndicator

This commit is contained in:
FooIbar committed 2023-11-26 21:08:18 +08:00
1 parent 2a667e62f4
commit a4e9291bf9
4 files changed
+140 -89

No files matched your search

@@ -1,4 +1,4 @@
package com.hippo.ehviewer.ui.scene
package com.hippo.ehviewer.ui.main
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -12,12 +12,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
@@ -28,7 +30,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -57,6 +58,8 @@ import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.big.SadAndroid
import com.hippo.ehviewer.ui.WebViewActivity
import com.hippo.ehviewer.ui.scene.collectDetailSizeAsState
import com.hippo.ehviewer.ui.scene.navAnimated
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid
import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid
import com.hippo.ehviewer.ui.tools.LocalDialogState
@@ -69,15 +72,15 @@ fun GalleryList(
data: LazyPagingItems<BaseGalleryInfo>,
contentModifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
listMode: Int = 0,
listMode: Int,
detailListState: LazyGridState = rememberLazyGridState(),
detailItemContent: @Composable (LazyGridItemScope.(BaseGalleryInfo) -> Unit),
thumbListState: LazyStaggeredGridState = rememberLazyStaggeredGridState(),
thumbItemContent: @Composable (LazyStaggeredGridItemScope.(BaseGalleryInfo) -> Unit) = {},
refreshState: PullToRefreshState? = null,
onRefresh: suspend CoroutineScope.() -> Unit = {},
onLoading: suspend CoroutineScope.() -> Unit = {},
navigator: NavController? = null,
thumbItemContent: @Composable (LazyStaggeredGridItemScope.(BaseGalleryInfo) -> Unit),
refreshState: PullToRefreshState,
onRefresh: suspend CoroutineScope.() -> Unit,
onLoading: suspend CoroutineScope.() -> Unit,
navigator: NavController,
) {
val layoutDirection = LocalLayoutDirection.current
val marginH = dimensionResource(id = R.dimen.gallery_list_margin_h)
@@ -88,11 +91,7 @@ fun GalleryList(
start = contentPadding.calculateStartPadding(layoutDirection) + marginH,
end = contentPadding.calculateEndPadding(layoutDirection) + marginH,
)
val combinedModifier = if (refreshState != null) {
contentModifier.nestedScroll(refreshState.nestedScrollConnection)
} else {
contentModifier
}
val combinedModifier = contentModifier.nestedScroll(refreshState.nestedScrollConnection)
Box(modifier = modifier.fillMaxSize()) {
if (listMode == 0) {
val columnWidth by collectDetailSizeAsState()
@@ -114,6 +113,13 @@ fun GalleryList(
detailItemContent(info)
}
}
if (data.loadState.append !is LoadState.NotLoading) {
item(span = { GridItemSpan(maxLineSpan) }) {
LoadStateIndicator(state = data.loadState.append) {
data.retry()
}
}
}
}
} else {
val gridInterval = dimensionResource(R.dimen.gallery_grid_interval)
@@ -136,92 +142,98 @@ fun GalleryList(
thumbItemContent(info)
}
}
if (data.loadState.append !is LoadState.NotLoading) {
item(span = StaggeredGridItemSpan.FullLine) {
LoadStateIndicator(state = data.loadState.append) {
data.retry()
}
}
}
}
}
if (refreshState != null) {
if (refreshState.isRefreshing) {
LaunchedEffect(Unit) {
var isLoading by remember { mutableStateOf(false) }
if (refreshState.isRefreshing) {
LaunchedEffect(Unit) {
if (data.loadState.prepend.endOfPaginationReached) {
onRefresh()
}
}
val dialogState = LocalDialogState.current
var refreshing by remember { mutableStateOf(false) }
when (val state = data.loadState.refresh) {
is LoadState.Loading -> if (!refreshState.isRefreshing) {
LaunchedEffect(Unit) {
onLoading()
}
LaunchedEffect(Unit) {
if (listMode == 0) {
detailListState.scrollToItem(0)
} else {
thumbListState.scrollToItem(0)
}
}
Surface {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}
} else {
refreshing = true
data.retry()
}
is LoadState.Error -> {
Surface {
LaunchedEffect(state) {
if (state.error.cause is CloudflareBypassException) {
dialogState.awaitPermissionOrCancel(title = R.string.cloudflare_bypass_failed) {
Text(text = stringResource(id = R.string.open_in_webview))
}
navigator?.navAnimated(R.id.webView, bundleOf(WebViewActivity.KEY_URL to EhUrl.host))
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Column(
modifier = Modifier.widthIn(max = 228.dp)
.clip(ShapeDefaults.Small).clickable { data.retry() },
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = EhIcons.Big.Default.SadAndroid,
contentDescription = null,
modifier = Modifier.padding(16.dp).size(120.dp),
tint = MaterialTheme.colorScheme.tertiary,
)
Text(
text = ExceptionUtils.getReadableString(state.error),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
)
}
}
}
SideEffect {
if (refreshing) {
refreshState.endRefresh()
refreshing = false
}
}
}
is LoadState.NotLoading -> SideEffect {
// Don't use `refreshState.isRefreshing` here because recomposition may happen
// before loading state changes
if (refreshing) {
}
LaunchedEffect(data.loadState) {
if (data.loadState.prepend is LoadState.Loading ||
data.loadState.refresh is LoadState.Loading
) {
isLoading = true
} else {
if (isLoading) {
refreshState.endRefresh()
refreshing = false
isLoading = false
}
}
}
}
val dialogState = LocalDialogState.current
when (val state = data.loadState.refresh) {
is LoadState.Loading -> if (!refreshState.isRefreshing) {
LaunchedEffect(Unit) {
onLoading()
}
LaunchedEffect(Unit) {
if (listMode == 0) {
detailListState.scrollToItem(0)
} else {
thumbListState.scrollToItem(0)
}
}
Surface {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}
}
is LoadState.Error -> {
Surface {
LaunchedEffect(state) {
if (state.error.cause is CloudflareBypassException) {
dialogState.awaitPermissionOrCancel(title = R.string.cloudflare_bypass_failed) {
Text(text = stringResource(id = R.string.open_in_webview))
}
navigator.navAnimated(R.id.webView, bundleOf(WebViewActivity.KEY_URL to EhUrl.host))
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Column(
modifier = Modifier.widthIn(max = 228.dp)
.clip(ShapeDefaults.Small).clickable { data.retry() },
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = EhIcons.Big.Default.SadAndroid,
contentDescription = null,
modifier = Modifier.padding(16.dp).size(120.dp),
tint = MaterialTheme.colorScheme.tertiary,
)
Text(
text = ExceptionUtils.getReadableString(state.error),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
)
}
}
}
}
is LoadState.NotLoading -> Unit
}
}
}
@@ -0,0 +1,37 @@
package com.hippo.ehviewer.ui.main
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.paging.LoadState
import com.hippo.ehviewer.R
import com.hippo.ehviewer.util.ExceptionUtils
@Composable
fun LoadStateIndicator(
modifier: Modifier = Modifier,
state: LoadState,
retry: () -> Unit,
) {
when (state) {
is LoadState.Loading -> {
LinearProgressIndicator(modifier = modifier)
}
is LoadState.Error -> {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = ExceptionUtils.getReadableString(state.error))
Button(onClick = retry) {
Text(text = stringResource(id = R.string.action_retry))
}
}
}
is LoadState.NotLoading -> Unit
}
}
@@ -79,6 +79,7 @@ import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.main.FabLayout
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
import com.hippo.ehviewer.ui.main.GalleryList
import com.hippo.ehviewer.ui.startDownload
import com.hippo.ehviewer.ui.tools.LocalDialogState
import com.hippo.ehviewer.ui.tools.rememberInVM
@@ -145,6 +145,7 @@ import com.hippo.ehviewer.ui.main.FAB_ANIMATE_TIME
import com.hippo.ehviewer.ui.main.FabLayout
import com.hippo.ehviewer.ui.main.GalleryInfoGridItem
import com.hippo.ehviewer.ui.main.GalleryInfoListItem
import com.hippo.ehviewer.ui.main.GalleryList
import com.hippo.ehviewer.ui.main.ImageSearch
import com.hippo.ehviewer.ui.main.NormalSearch
import com.hippo.ehviewer.ui.main.SearchAdvanced