Extract common part of GalleryList and Favorites (#338)

* Extract common part of GalleryList and Favorites

* Scroll to top after refreshing
This commit is contained in:
FooIbar authored and GitHub committed 2023-11-25 15:15:28 +00:00
1 parent dd06d82cd9
commit fec2c39364
4 files changed
+368 -346

No files matched your search

@@ -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)
}
@@ -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
@@ -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<BaseGalleryInfo>,
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
}
}
}
}
}
}
@@ -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<String, BaseGalleryInfo>() {
override fun getRefreshKey(state: PagingState<String, BaseGalleryInfo>): 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) {