diff --git a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt index 68a9d041d..5aff2dfaa 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt @@ -2,8 +2,6 @@ package io.legado.app.ui.main import android.content.Intent import android.os.Build -import android.os.SystemClock -import android.view.ViewConfiguration import androidx.activity.ComponentActivity import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibilityScope @@ -197,26 +195,20 @@ fun MainScreen( orientation = Orientation.Horizontal, ) var bookshelfScrollToTopRequest by remember { mutableLongStateOf(0L) } - var lastBookshelfNavClickTime by remember { mutableLongStateOf(0L) } - fun requestBookshelfScrollToTop(index: Int) { + var isBookshelfAtTop by remember { mutableStateOf(true) } + fun requestBookshelfScrollToTop() { bookshelfScrollToTopRequest++ - coroutineScope.launch { - pagerState.animateScrollToPage(index) - } } fun handleMainDestinationClick(index: Int, destination: MainDestination) { - if (destination == MainDestination.Bookshelf) { - val now = SystemClock.uptimeMillis() - if ( - (pagerState.currentPage == index || pagerState.targetPage == index) && - now - lastBookshelfNavClickTime <= ViewConfiguration.getDoubleTapTimeout() - ) { - requestBookshelfScrollToTop(index) - lastBookshelfNavClickTime = 0L - return - } - lastBookshelfNavClickTime = now + if ( + destination == MainDestination.Bookshelf && + pagerState.currentPage == index && + pagerState.targetPage == index && + !isBookshelfAtTop + ) { + requestBookshelfScrollToTop() + return } coroutineScope.launch { pagerState.animateScrollToPage(index) @@ -434,6 +426,9 @@ fun MainScreen( MainDestination.Bookshelf -> BookshelfScreen( scrollToTopRequest = bookshelfScrollToTopRequest, + onScrollStateChanged = { isAtTop -> + isBookshelfAtTop = isAtTop + }, onBookClick = { book -> context.startActivityForBook(book) }, @@ -529,13 +524,6 @@ fun MainScreen( onClick = { handleMainDestinationClick(index, destination) }, - onDoubleClick = if (destination == MainDestination.Bookshelf) { - { - requestBookshelfScrollToTop(index) - } - } else { - null - }, modifier = Modifier .defaultMinSize(minWidth = 76.dp) .semantics(mergeDescendants = true) { diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt index c460367df..0fa9c8e3d 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt @@ -44,6 +44,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.lazy.grid.rememberLazyGridState @@ -76,6 +77,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -156,6 +158,7 @@ import sh.calvin.reorderable.rememberReorderableLazyGridState fun BookshelfScreen( viewModel: BookshelfViewModel = koinViewModel(), scrollToTopRequest: Long = 0L, + onScrollStateChanged: (isAtTop: Boolean) -> Unit = {}, onBookClick: (BookShelfItem) -> Unit, onBookLongClick: (book: BookShelfItem, sharedCoverKey: String?) -> Unit, onNavigateToSearch: (String) -> Unit, @@ -233,6 +236,14 @@ fun BookshelfScreen( initialPage = uiState.selectedGroupIndex.coerceAtLeast(0), pageCount = { uiState.groups.size } ) + val folderGridState = rememberLazyGridState() + val standaloneSearchGridState = rememberLazyGridState() + val groupGridStates = mutableMapOf() + uiState.groups.forEach { group -> + key(group.groupId) { + groupGridStates[group.groupId] = rememberLazyGridState() + } + } val latestGroups by rememberUpdatedState(uiState.groups) val latestSelectedGroupId by rememberUpdatedState(uiState.selectedGroupId) @@ -281,6 +292,20 @@ fun BookshelfScreen( uiState.isSearch && uiState.groups.none { it.groupId == currentGroupId } } } + val isShowingFolderRoot = + bookGroupStyle == 2 && isInFolderRoot && !isUsingStandaloneSearchGroup + LaunchedEffect(folderGridState, isShowingFolderRoot) { + if (isShowingFolderRoot) { + snapshotFlow { !folderGridState.canScrollBackward } + .distinctUntilChanged() + .collect(onScrollStateChanged) + } + } + LaunchedEffect(scrollToTopRequest, isShowingFolderRoot) { + if (scrollToTopRequest > 0L && isShowingFolderRoot) { + folderGridState.animateScrollToItem(0) + } + } val currentGroupBookCount by remember { derivedStateOf { uiState.currentGroupBookCount } } val clearSelection = { @@ -726,6 +751,9 @@ fun BookshelfScreen( } ) { isRoot -> if (uiState.groups.isEmpty() && !uiState.isSearch) { + LaunchedEffect(Unit) { + onScrollStateChanged(true) + } if (!uiState.isInitialLoading) { EmptyMessage( modifier = Modifier @@ -743,6 +771,7 @@ fun BookshelfScreen( val isGridMode = bookshelfFolderLayoutMode != 0 FastScrollLazyVerticalGrid( columns = GridCells.Fixed(folderColumns.coerceAtLeast(1)), + state = folderGridState, modifier = Modifier .fillMaxSize() .then( @@ -815,6 +844,8 @@ fun BookshelfScreen( if (isUsingStandaloneSearchGroup) { BookshelfPage( scrollToTopRequest = scrollToTopRequest, + gridState = standaloneSearchGridState, + onScrollStateChanged = onScrollStateChanged, paddingValues = paddingValues, books = uiState.items, uiState = uiState, @@ -862,6 +893,8 @@ fun BookshelfScreen( isSelectedGroup BookshelfPage( scrollToTopRequest = scrollToTopRequest, + gridState = groupGridStates.getValue(group.groupId), + onScrollStateChanged = onScrollStateChanged, paddingValues = paddingValues, books = books, uiState = uiState, @@ -1212,6 +1245,8 @@ private data class BookshelfEditStickySummary( @Composable fun BookshelfPage( scrollToTopRequest: Long, + gridState: LazyGridState, + onScrollStateChanged: (isAtTop: Boolean) -> Unit, paddingValues: PaddingValues, books: ImmutableList, uiState: BookshelfUiState, @@ -1230,6 +1265,11 @@ fun BookshelfPage( sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, ) { + LaunchedEffect(books.isEmpty(), isCurrentPage) { + if (books.isEmpty() && isCurrentPage) { + onScrollStateChanged(true) + } + } if (books.isEmpty()) { if (!isCurrentPage) return if (uiState.isSearch) { @@ -1297,12 +1337,18 @@ fun BookshelfPage( val gridInnerHorizontalPadding = totalHorizontalPadding / 2 val hapticFeedback = LocalHapticFeedback.current val displayBooks = draggingBooks ?: pendingSavedBooks ?: books - val gridState = rememberLazyGridState() LaunchedEffect(scrollToTopRequest, isCurrentPage) { if (scrollToTopRequest > 0L && isCurrentPage) { gridState.animateScrollToItem(0) } } + LaunchedEffect(gridState, isCurrentPage) { + if (isCurrentPage) { + snapshotFlow { !gridState.canScrollBackward } + .distinctUntilChanged() + .collect(onScrollStateChanged) + } + } val reorderableState = rememberReorderableLazyGridState(gridState) { from, to -> if (canReorderBooks) { onMoveBook(from.index, to.index, displayBooks) diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index 43e20f052..220e4c517 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -52,6 +52,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect @@ -65,6 +66,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -140,13 +142,13 @@ class BookshelfViewModel( protected val _eventChannel = Channel() val events = _eventChannel.receiveAsFlow() - val groupsFlow: StateFlow> = bookGroupRepository.flowShow() + val groupsFlow: SharedFlow> = bookGroupRepository.flowShow() .onEach { if (it.isNotEmpty()) { isInitialLoadingFlow.value = false } } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) + .shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1) val allGroupsFlow: StateFlow> = bookGroupRepository.flowAll() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) @@ -195,7 +197,7 @@ class BookshelfViewModel( }.distinctUntilChanged().flowOn(Dispatchers.Default) @OptIn(ExperimentalCoroutinesApi::class) - private val allGroupBooksImmutableFlow: StateFlow>> = + private val allGroupBooksImmutableFlow: Flow>> = combine(groupsFlow, sortConfigFlow) { groups, sortConfig -> groups to sortConfig }.flatMapLatest { (groups, sortConfig) -> @@ -220,7 +222,6 @@ class BookshelfViewModel( } }.distinctUntilChanged() .flowOn(Dispatchers.Default) - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentMapOf()) private val visibleBooksFlow: Flow> = combine( booksFlow, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/FloatingBottomBar.kt b/app/src/main/java/io/legado/app/ui/widget/components/FloatingBottomBar.kt index b2a3187e8..f31c834ba 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/FloatingBottomBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/FloatingBottomBar.kt @@ -9,7 +9,6 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.EaseOut import androidx.compose.animation.core.spring import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -78,7 +77,6 @@ val LocalFloatingBottomBarTabScale = staticCompositionLocalOf { { 1f } } @Composable fun RowScope.FloatingBottomBarItem( onClick: () -> Unit, - onDoubleClick: (() -> Unit)? = null, modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit ) { @@ -86,23 +84,11 @@ fun RowScope.FloatingBottomBarItem( Column( modifier .clip(ContinuousCapsule) - .then( - if (onDoubleClick != null) { - Modifier.combinedClickable( - interactionSource = null, - indication = null, - role = Role.Tab, - onClick = onClick, - onDoubleClick = onDoubleClick, - ) - } else { - Modifier.clickable( - interactionSource = null, - indication = null, - role = Role.Tab, - onClick = onClick, - ) - } + .clickable( + interactionSource = null, + indication = null, + role = Role.Tab, + onClick = onClick, ) .fillMaxHeight() .weight(1f)