diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/data/ListUrlBuilder.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/data/ListUrlBuilder.kt index b31936cc3..32e5393ff 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/data/ListUrlBuilder.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/data/ListUrlBuilder.kt @@ -35,6 +35,7 @@ data class ListUrlBuilder( var mode: Int = MODE_NORMAL, private var mPrev: String? = null, var mNext: String? = null, + // Reset to null after initial loading var mJumpTo: String? = null, var category: Int = EhUtils.NONE, private var mKeyword: String? = null, @@ -48,6 +49,23 @@ data class ListUrlBuilder( var isOnlySearchCovers: Boolean = false, ) : Parcelable { + fun reset() { + mode = MODE_NORMAL + mPrev = null + mNext = null + mJumpTo = null + this.category = EhUtils.NONE + mKeyword = null + advanceSearch = -1 + minRating = -1 + pageFrom = -1 + pageTo = -1 + imagePath = null + isUseSimilarityScan = false + isOnlySearchCovers = false + hash = null + } + fun setIndex(index: String?, isNext: Boolean = true) { mNext = index?.takeIf { isNext } mPrev = index?.takeUnless { isNext } @@ -63,7 +81,8 @@ data class ListUrlBuilder( mKeyword = keyword } - constructor(q: QuickSearch) : this() { + fun set(q: QuickSearch) { + reset() mode = q.mode this.category = q.category mKeyword = q.keyword @@ -355,9 +374,7 @@ data class ListUrlBuilder( buildString { append(EhUrl.HOST_E) append("toplist.php?tl=") - mKeyword.orEmpty().let { - append(encodeUTF8(it)) - } + append(mKeyword!!) mJumpTo?.let { append("&p=").append(it) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesFragment.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesFragment.kt index e2d4ec23f..de2629bd3 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesFragment.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/FavoritesFragment.kt @@ -5,24 +5,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.foundation.clickable -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.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text2.input.rememberTextFieldState import androidx.compose.foundation.verticalScroll @@ -34,13 +23,10 @@ import androidx.compose.material.icons.filled.DoneAll import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.outlined.FolderSpecial -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ShapeDefaults -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState @@ -56,19 +42,13 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.lifecycle.viewModelScope import androidx.navigation.NavController @@ -80,8 +60,6 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemContentType -import androidx.paging.compose.itemKey import com.google.android.material.datepicker.CalendarConstraints import com.google.android.material.datepicker.CompositeDateValidator import com.google.android.material.datepicker.DateValidatorPointBackward @@ -96,18 +74,14 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo import com.hippo.ehviewer.client.data.FavListUrlBuilder import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.icons.EhIcons -import com.hippo.ehviewer.icons.big.SadAndroid import com.hippo.ehviewer.icons.filled.GoTo 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.startDownload -import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid -import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid import com.hippo.ehviewer.ui.tools.LocalDialogState import com.hippo.ehviewer.ui.tools.rememberInVM -import com.hippo.ehviewer.util.ExceptionUtils import com.hippo.ehviewer.util.findActivity import com.hippo.ehviewer.util.mapToLongArray import com.ramcosta.composedestinations.annotation.Destination @@ -248,12 +222,6 @@ fun FavouritesScreen(navigator: NavController) { data.loadState.refresh is LoadState.NotLoading } - if (refreshState.isRefreshing) { - LaunchedEffect(Unit) { - refresh() - } - } - val searchBarConnection = remember { object : NestedScrollConnection { override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { @@ -262,7 +230,6 @@ fun FavouritesScreen(navigator: NavController) { } } } - val combinedModifier = Modifier.nestedScroll(searchBarConnection).nestedScroll(refreshState.nestedScrollConnection) var expanded by remember { mutableStateOf(false) } var hidden by remember { mutableStateOf(false) } @@ -288,167 +255,73 @@ fun FavouritesScreen(navigator: NavController) { } }, ) { - val layoutDirection = LocalLayoutDirection.current - val marginH = dimensionResource(id = R.dimen.gallery_list_margin_h) - val marginV = dimensionResource(id = R.dimen.gallery_list_margin_v) - val realPadding = PaddingValues( - top = it.calculateTopPadding() + marginV, - bottom = it.calculateBottomPadding() + marginV, - start = it.calculateStartPadding(layoutDirection) + marginH, - end = it.calculateEndPadding(layoutDirection) + marginH, - ) val listMode by Settings.listMode.collectAsState() - Box(modifier = Modifier.fillMaxSize()) { - if (listMode == 0) { - val height by collectListThumbSizeAsState() - val showPages = Settings.showGalleryPages - val columnWidth by collectDetailSizeAsState() - FastScrollLazyVerticalGrid( - columns = GridCells.Adaptive(columnWidth), - modifier = combinedModifier, - contentPadding = realPadding, - verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)), - horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)), - ) { - items( - count = data.itemCount, - key = data.itemKey(key = { item -> item.gid }), - contentType = data.itemContentType(), - ) { index -> - val info = data[index] - if (info != null) { - val checked = info.gid in checkedInfoMap - CheckableItem(checked = checked) { - GalleryInfoListItem( - onClick = { - if (selectMode) { - if (checked) { - checkedInfoMap.remove(info.gid) - } else { - checkedInfoMap[info.gid] = info - } - } else { - navigator.navAnimated( - R.id.galleryDetailScene, - bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)), - ) - } - }, - onLongClick = { - selectMode = true - checkedInfoMap[info.gid] = info - }, - info = info, - isInFavScene = true, - showPages = showPages, - modifier = Modifier.height(height), + val height by collectListThumbSizeAsState() + val showPages = Settings.showGalleryPages + GalleryList( + data = data, + contentModifier = Modifier.nestedScroll(searchBarConnection), + contentPadding = it, + listMode = listMode, + detailItemContent = { info -> + val checked = info.gid in checkedInfoMap + CheckableItem(checked = checked) { + GalleryInfoListItem( + onClick = { + if (selectMode) { + if (checked) { + checkedInfoMap.remove(info.gid) + } else { + checkedInfoMap[info.gid] = info + } + } else { + navigator.navAnimated( + R.id.galleryDetailScene, + bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)), ) } - } - } + }, + onLongClick = { + selectMode = true + checkedInfoMap[info.gid] = info + }, + info = info, + isInFavScene = true, + showPages = showPages, + modifier = Modifier.height(height), + ) } - } else { - val gridInterval = dimensionResource(R.dimen.gallery_grid_interval) - val thumbWidth by Settings.thumbSizeDp.collectAsState() - FastScrollLazyVerticalStaggeredGrid( - columns = StaggeredGridCells.Adaptive(thumbWidth.dp), - modifier = combinedModifier, - verticalItemSpacing = gridInterval, - horizontalArrangement = Arrangement.spacedBy(gridInterval), - contentPadding = realPadding, - ) { - items( - count = data.itemCount, - key = data.itemKey(key = { item -> item.gid }), - contentType = data.itemContentType(), - ) { index -> - val info = data[index] - if (info != null) { - val checked = info.gid in checkedInfoMap - CheckableItem(checked = checked) { - GalleryInfoGridItem( - onClick = { - if (selectMode) { - if (checked) { - checkedInfoMap.remove(info.gid) - } else { - checkedInfoMap[info.gid] = info - } - } else { - navigator.navAnimated( - R.id.galleryDetailScene, - bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)), - ) - } - }, - onLongClick = { - selectMode = true - checkedInfoMap[info.gid] = info - }, - info = info, + }, + thumbItemContent = { info -> + val checked = info.gid in checkedInfoMap + CheckableItem(checked = checked) { + GalleryInfoGridItem( + onClick = { + if (selectMode) { + if (checked) { + checkedInfoMap.remove(info.gid) + } else { + checkedInfoMap[info.gid] = info + } + } else { + navigator.navAnimated( + R.id.galleryDetailScene, + bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)), ) } - } - } + }, + onLongClick = { + selectMode = true + checkedInfoMap[info.gid] = info + }, + info = info, + ) } - } - - var refreshing by remember { mutableStateOf(false) } - when (val state = data.loadState.refresh) { - is LoadState.Loading -> if (!refreshState.isRefreshing) { - Surface { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } - } - } else { - refreshing = true - } - is LoadState.Error -> { - Surface { - 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) { - refreshState.endRefresh() - refreshing = false - } - } - } - } + }, + refreshState = refreshState, + onRefresh = { refresh() }, + navigator = navigator, + ) } // Explicitly reallocate fabBuilder lambda to recompose secondary fab diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryList.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryList.kt new file mode 100644 index 000000000..f10cdb8e9 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryList.kt @@ -0,0 +1,223 @@ +package com.hippo.ehviewer.ui.scene + +import androidx.compose.foundation.clickable +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.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +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.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.rememberLazyStaggeredGridState +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ShapeDefaults +import androidx.compose.material3.Surface +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 +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf +import androidx.navigation.NavController +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey +import com.hippo.ehviewer.R +import com.hippo.ehviewer.Settings +import com.hippo.ehviewer.client.EhUrl +import com.hippo.ehviewer.client.data.BaseGalleryInfo +import com.hippo.ehviewer.client.exception.CloudflareBypassException +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.tools.FastScrollLazyVerticalGrid +import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid +import com.hippo.ehviewer.ui.tools.LocalDialogState +import com.hippo.ehviewer.util.ExceptionUtils +import kotlinx.coroutines.CoroutineScope + +@Composable +fun GalleryList( + modifier: Modifier = Modifier, + data: LazyPagingItems, + contentModifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), + listMode: Int = 0, + detailListState: LazyGridState = rememberLazyGridState(), + detailItemContent: @Composable (LazyGridItemScope.(BaseGalleryInfo) -> Unit), + thumbListState: LazyStaggeredGridState = rememberLazyStaggeredGridState(), + thumbItemContent: @Composable (LazyStaggeredGridItemScope.(BaseGalleryInfo) -> Unit) = {}, + refreshState: PullToRefreshState? = null, + onRefresh: suspend CoroutineScope.() -> Unit = {}, + navigator: NavController? = null, +) { + val layoutDirection = LocalLayoutDirection.current + val marginH = dimensionResource(id = R.dimen.gallery_list_margin_h) + val marginV = dimensionResource(id = R.dimen.gallery_list_margin_v) + val realPadding = PaddingValues( + top = contentPadding.calculateTopPadding() + marginV, + bottom = contentPadding.calculateBottomPadding() + marginV, + start = contentPadding.calculateStartPadding(layoutDirection) + marginH, + end = contentPadding.calculateEndPadding(layoutDirection) + marginH, + ) + val combinedModifier = if (refreshState != null) { + contentModifier.nestedScroll(refreshState.nestedScrollConnection) + } else { + contentModifier + } + Box(modifier = modifier.fillMaxSize()) { + if (listMode == 0) { + val columnWidth by collectDetailSizeAsState() + FastScrollLazyVerticalGrid( + columns = GridCells.Adaptive(columnWidth), + modifier = combinedModifier, + state = detailListState, + contentPadding = realPadding, + verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)), + horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)), + ) { + items( + count = data.itemCount, + key = data.itemKey(key = { item -> item.gid }), + contentType = data.itemContentType(), + ) { index -> + val info = data[index] + if (info != null) { + detailItemContent(info) + } + } + } + } else { + val gridInterval = dimensionResource(R.dimen.gallery_grid_interval) + val thumbWidth by Settings.thumbSizeDp.collectAsState() + FastScrollLazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(thumbWidth.dp), + modifier = combinedModifier, + state = thumbListState, + verticalItemSpacing = gridInterval, + horizontalArrangement = Arrangement.spacedBy(gridInterval), + contentPadding = realPadding, + ) { + items( + count = data.itemCount, + key = data.itemKey(key = { item -> item.gid }), + contentType = data.itemContentType(), + ) { index -> + val info = data[index] + if (info != null) { + thumbItemContent(info) + } + } + } + } + + if (refreshState != null) { + if (refreshState.isRefreshing) { + LaunchedEffect(Unit) { + 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) { + if (listMode == 0) { + detailListState.scrollToItem(0) + } else { + thumbListState.scrollToItem(0) + } + } + Surface { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } + } else { + refreshing = true + } + + 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) { + refreshState.endRefresh() + refreshing = false + } + } + } + } + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListFragment.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListFragment.kt index 135bbe723..b41a6b0d0 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListFragment.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/scene/GalleryListFragment.kt @@ -13,10 +13,8 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable -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.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues @@ -28,16 +26,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text2.input.rememberTextFieldState @@ -52,7 +46,6 @@ import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Reorder import androidx.compose.material.icons.filled.Translate import androidx.compose.material.icons.outlined.Bookmarks -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DismissDirection import androidx.compose.material3.DismissValue import androidx.compose.material3.ElevatedCard @@ -63,7 +56,6 @@ import androidx.compose.material3.LeadingIconTab import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryTabRow -import androidx.compose.material3.ShapeDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SwipeToDismissBox @@ -76,7 +68,6 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -89,7 +80,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale import androidx.compose.ui.geometry.Offset @@ -104,7 +94,6 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach @@ -120,8 +109,6 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import androidx.paging.cachedIn import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemContentType -import androidx.paging.compose.itemKey import com.google.android.material.datepicker.CalendarConstraints import com.google.android.material.datepicker.CalendarConstraints.DateValidator import com.google.android.material.datepicker.CompositeDateValidator @@ -133,7 +120,6 @@ import com.hippo.ehviewer.R import com.hippo.ehviewer.Settings import com.hippo.ehviewer.client.EhEngine import com.hippo.ehviewer.client.EhTagDatabase -import com.hippo.ehviewer.client.EhUrl import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.data.BaseGalleryInfo import com.hippo.ehviewer.client.data.ListUrlBuilder @@ -144,17 +130,14 @@ import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TAG import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_UPLOADER import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_WHATS_HOT -import com.hippo.ehviewer.client.exception.CloudflareBypassException import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser import com.hippo.ehviewer.client.parser.GalleryPageUrlParser import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.dao.QuickSearch import com.hippo.ehviewer.icons.EhIcons -import com.hippo.ehviewer.icons.big.SadAndroid import com.hippo.ehviewer.icons.filled.GoTo import com.hippo.ehviewer.image.Image.Companion.decodeBitmap import com.hippo.ehviewer.ui.MainActivity -import com.hippo.ehviewer.ui.WebViewActivity import com.hippo.ehviewer.ui.doGalleryInfoAction import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder import com.hippo.ehviewer.ui.legacy.FAB_ANIMATE_TIME @@ -167,15 +150,12 @@ import com.hippo.ehviewer.ui.main.NormalSearch import com.hippo.ehviewer.ui.main.SearchAdvanced import com.hippo.ehviewer.ui.scene.GalleryListFragment.Companion.toStartArgs import com.hippo.ehviewer.ui.tools.Deferred -import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid -import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid import com.hippo.ehviewer.ui.tools.LocalDialogState import com.hippo.ehviewer.ui.tools.LocalTouchSlopProvider import com.hippo.ehviewer.ui.tools.animateFloatMergePredictiveBackAsState import com.hippo.ehviewer.ui.tools.observed import com.hippo.ehviewer.ui.tools.rememberInVM import com.hippo.ehviewer.util.AppConfig -import com.hippo.ehviewer.util.ExceptionUtils import com.hippo.ehviewer.util.findActivity import com.hippo.ehviewer.util.getParcelableCompat import com.hippo.ehviewer.util.isAtLeastOMR1 @@ -248,7 +228,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) { val exHint = stringResource(R.string.gallery_list_search_bar_hint_exhentai) val searchBarHint = remember { if (EhUtils.isExHentai) exHint else ehHint } val suitableTitle = getSuitableTitleForUrlBuilder(urlBuilder) - val data = rememberInVM(urlBuilder) { + val data = rememberInVM { Pager(PagingConfig(25)) { object : PagingSource() { override fun getRefreshKey(state: PagingState): String? = null @@ -264,6 +244,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) { }.onFailure { return@withIOContext LoadResult.Error(it) }.onSuccess { + urlBuilder.mJumpTo = null return@withIOContext LoadResult.Page(it.galleryInfoList, prev?.toString(), next?.toString()) } } @@ -331,7 +312,8 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) { ListItem( modifier = Modifier.clickable { Settings.recentToplist = keyword - urlBuilder = ListUrlBuilder(MODE_TOPLIST, mKeyword = keyword) + urlBuilder.keyword = keyword + data.refresh() showSearchLayout = false coroutineScope.launch { sheetState.close() } }, @@ -485,9 +467,10 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) { ListItem( modifier = Modifier.clickable { if (urlBuilder.mode == MODE_WHATS_HOT) { - navigator.navAnimated(R.id.galleryListScene, ListUrlBuilder(item).toStartArgs()) + navigator.navAnimated(R.id.galleryListScene, ListUrlBuilder().apply { set(item) }.toStartArgs()) } else { - urlBuilder = ListUrlBuilder(item) + urlBuilder.set(item) + data.refresh() } showSearchLayout = false coroutineScope.launch { sheetState.close() } @@ -556,14 +539,6 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) { data.loadState.refresh is LoadState.NotLoading } - if (refreshState.isRefreshing) { - LaunchedEffect(Unit) { - urlBuilder.setIndex(null, true) - urlBuilder.mJumpTo = null - data.refresh() - } - } - var hidden by remember { mutableStateOf(false) } val searchBarConnection = remember { val slop = ViewConfiguration.get(context).scaledTouchSlop @@ -580,7 +555,6 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) { } } } - val combinedModifier = Modifier.nestedScroll(searchBarConnection).nestedScroll(refreshState.nestedScrollConnection) val openGalleryKeyword = stringResource(R.string.gallery_list_search_bar_open_gallery) abstract class UrlSuggestion : Suggestion() { @@ -679,7 +653,10 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) { delay(300) withUIContext { navigator.navAnimated(R.id.galleryListScene, builder.toStartArgs()) } } - else -> urlBuilder = builder + else -> { + urlBuilder = builder + data.refresh() + } } showSearchLayout = false }, @@ -711,19 +688,12 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) { val layoutDirection = LocalLayoutDirection.current val marginH = dimensionResource(id = R.dimen.gallery_list_margin_h) val marginV = dimensionResource(id = R.dimen.gallery_list_margin_v) - val realPadding = PaddingValues( - top = paddingValues.calculateTopPadding() + marginV, - bottom = paddingValues.calculateBottomPadding() + marginV, - start = paddingValues.calculateStartPadding(layoutDirection) + marginH, - end = paddingValues.calculateEndPadding(layoutDirection) + marginH, - ) - Column( modifier = Modifier.imePadding().verticalScroll(rememberScrollState()) .padding( - top = realPadding.calculateTopPadding(), - start = realPadding.calculateStartPadding(layoutDirection), - end = realPadding.calculateEndPadding(layoutDirection), + top = paddingValues.calculateTopPadding() + marginV, + start = paddingValues.calculateStartPadding(layoutDirection) + marginH, + end = paddingValues.calculateEndPadding(layoutDirection) + marginH, bottom = 8.dp, ) .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Bottom)) @@ -820,118 +790,58 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) { } } - Box(modifier = Modifier.fillMaxSize().scale(animatedSearchLayout).alpha(animatedSearchLayout)) { - when (val loadState = data.loadState.refresh) { - is LoadState.Loading -> if (data.itemCount == 0) { - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) - } - is LoadState.Error -> { - LaunchedEffect(loadState) { - if (loadState.error.cause is CloudflareBypassException) { - dialogState.awaitPermissionOrCancel(title = R.string.cloudflare_bypass_failed) { - Text(text = stringResource(id = R.string.open_in_webview)) - } - withUIContext { navigator.navAnimated(R.id.webView, bundleOf(WebViewActivity.KEY_URL to EhUrl.host)) } - } - } - Column( - modifier = Modifier.align(Alignment.Center).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, + val height by collectListThumbSizeAsState() + val showPages = Settings.showGalleryPages + GalleryList( + modifier = Modifier.scale(animatedSearchLayout).alpha(animatedSearchLayout), + data = data, + contentModifier = Modifier.nestedScroll(searchBarConnection), + contentPadding = paddingValues, + listMode = listMode, + detailListState = listState, + detailItemContent = { info -> + GalleryInfoListItem( + onClick = { + navigator.navAnimated( + R.id.galleryDetailScene, + bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)), ) - Text( - text = ExceptionUtils.getReadableString(loadState.error), - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, + }, + onLongClick = { + coroutineScope.launch { + dialogState.doGalleryInfoAction(info, context) + } + }, + info = info, + isInFavScene = false, + showPages = showPages, + modifier = Modifier.height(height), + ) + }, + thumbListState = gridState, + thumbItemContent = { info -> + GalleryInfoGridItem( + onClick = { + navigator.navAnimated( + R.id.galleryDetailScene, + bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)), ) - } - } - is LoadState.NotLoading -> SideEffect { - refreshState.endRefresh() - } - } - if (listMode == 0) { - val height by collectListThumbSizeAsState() - val showPages = Settings.showGalleryPages - val columnWidth by collectDetailSizeAsState() - FastScrollLazyVerticalGrid( - columns = GridCells.Adaptive(columnWidth), - modifier = combinedModifier, - state = listState, - contentPadding = realPadding, - verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)), - horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)), - ) { - items( - count = data.itemCount, - key = data.itemKey(key = { item -> item.gid }), - contentType = data.itemContentType(), - ) { index -> - val info = data[index] - if (info != null) { - GalleryInfoListItem( - onClick = { - navigator.navAnimated( - R.id.galleryDetailScene, - bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)), - ) - }, - onLongClick = { - coroutineScope.launch { - dialogState.doGalleryInfoAction(info, context) - } - }, - info = info, - isInFavScene = false, - showPages = showPages, - modifier = Modifier.height(height), - ) + }, + onLongClick = { + coroutineScope.launch { + dialogState.doGalleryInfoAction(info, context) } - } - } - } else { - val gridInterval = dimensionResource(R.dimen.gallery_grid_interval) - val thumbWidth by Settings.thumbSizeDp.collectAsState() - FastScrollLazyVerticalStaggeredGrid( - columns = StaggeredGridCells.Adaptive(thumbWidth.dp), - modifier = combinedModifier, - state = gridState, - verticalItemSpacing = gridInterval, - horizontalArrangement = Arrangement.spacedBy(gridInterval), - contentPadding = realPadding, - ) { - items( - count = data.itemCount, - key = data.itemKey(key = { item -> item.gid }), - contentType = data.itemContentType(), - ) { index -> - val info = data[index] - if (info != null) { - GalleryInfoGridItem( - onClick = { - navigator.navAnimated( - R.id.galleryDetailScene, - bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info)), - ) - }, - onLongClick = { - coroutineScope.launch { - dialogState.doGalleryInfoAction(info, context) - } - }, - info = info, - ) - } - } - } - } - } + }, + info = info, + ) + }, + refreshState = refreshState, + onRefresh = { + urlBuilder.setIndex(null) + data.refresh() + }, + navigator = navigator, + ) } val gotoTitle = stringResource(R.string.go_to) @@ -944,8 +854,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: NavController) { autoCancel = true, ) { onClick(Icons.Default.Refresh) { - urlBuilder.setIndex(null, true) - urlBuilder.mJumpTo = null + urlBuilder.setIndex(null) data.refresh() } if (urlBuilder.mode != MODE_WHATS_HOT) {