Просмотр исходного кода

fix: improve manual sort drag visual feedback and selection handling

shilimesfriend 4 месяцев назад
Родитель
Сommit
9e6feb9f67
1 измененных файлов с 1417 добавлено и 1 удалено
  1. 1417 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

+ 1417 - 1
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -1 +1,1417 @@
-PLACEHOLDER
+package io.legado.app.ui.main.bookshelf
+
+import android.content.ClipData
+import android.content.res.Configuration
+import android.net.Uri
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.ManagedActivityResultLauncher
+import androidx.activity.compose.PredictiveBackHandler
+import androidx.activity.compose.ReportDrawnWhen
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.SeekableTransitionState
+import androidx.compose.animation.core.rememberTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBars
+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.items
+import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
+import androidx.compose.material.icons.filled.Bookmarks
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.CloudDownload
+import androidx.compose.material.icons.filled.Download
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.GridView
+import androidx.compose.material.icons.filled.History
+import androidx.compose.material.icons.filled.Link
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Save
+import androidx.compose.material.icons.filled.SelectAll
+import androidx.compose.material.icons.filled.UploadFile
+import androidx.compose.material.icons.filled.Wifi
+import androidx.compose.material.icons.outlined.ViewCarousel
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+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.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.ClipEntry
+import androidx.compose.ui.platform.LocalClipboard
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import io.legado.app.R
+import io.legado.app.base.BaseRuleEvent
+import io.legado.app.data.entities.BookGroup
+import io.legado.app.ui.book.group.GroupEditSheet
+import io.legado.app.ui.book.info.GroupSelectSheet
+import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
+import io.legado.app.ui.config.themeConfig.ThemeConfig
+import io.legado.app.ui.main.bookCoverSharedElementKey
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ThemeResolver
+import io.legado.app.ui.theme.adaptiveContentPaddingBookshelf
+import io.legado.app.ui.theme.adaptiveHorizontalPadding
+import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab
+import io.legado.app.ui.widget.components.AppPullToRefresh
+import io.legado.app.ui.widget.components.AppScaffold
+import io.legado.app.ui.widget.components.EmptyMessage
+import io.legado.app.ui.widget.components.SearchBar
+import io.legado.app.ui.widget.components.alert.AppAlertDialog
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
+import io.legado.app.ui.widget.components.button.series.SmallToggleButton
+import io.legado.app.ui.widget.components.button.series.ToggleStyle
+import io.legado.app.ui.widget.components.card.NormalCard
+import io.legado.app.ui.widget.components.card.TextCard
+import io.legado.app.ui.widget.components.divider.PillHeaderDivider
+import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
+import io.legado.app.ui.widget.components.icon.AppIcon
+import io.legado.app.ui.widget.components.icon.AppIcons
+import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
+import io.legado.app.ui.widget.components.lazylist.FastScrollLazyVerticalGrid
+import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
+import io.legado.app.ui.widget.components.log.AppLogSheet
+import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
+import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
+import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
+import io.legado.app.ui.widget.components.tabRow.AppTabRow
+import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
+import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.topbar.GlassTopAppBarScrollBehavior
+import io.legado.app.ui.widget.components.topbar.TopBarActionButton
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.ImmutableSet
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
+import org.koin.androidx.compose.koinViewModel
+import sh.calvin.reorderable.ReorderableItem
+import sh.calvin.reorderable.rememberReorderableLazyGridState
+
+@OptIn(
+    ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
+    ExperimentalMaterial3ExpressiveApi::class, ExperimentalSharedTransitionApi::class,
+    ExperimentalAnimationApi::class
+)
+@Composable
+fun BookshelfScreen(
+    viewModel: BookshelfViewModel = koinViewModel(),
+    onBookClick: (BookShelfItem) -> Unit,
+    onBookLongClick: (book: BookShelfItem, sharedCoverKey: String?) -> Unit,
+    onNavigateToSearch: (String) -> Unit,
+    onNavigateToRemoteImport: () -> Unit,
+    onNavigateToLocalImport: () -> Unit,
+    onNavigateToCache: (Long) -> Unit,
+    sharedTransitionScope: SharedTransitionScope? = null,
+    animatedVisibilityScope: AnimatedVisibilityScope? = null,
+) {
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+    val scope = rememberCoroutineScope()
+
+    ReportDrawnWhen { !uiState.isInitialLoading }
+
+    val clipboardManager = LocalClipboard.current
+    val snackbarHostState = remember { SnackbarHostState() }
+
+    LaunchedEffect(Unit) {
+        viewModel.events.collect { event ->
+            when (event) {
+                is BaseRuleEvent.ShowSnackbar -> {
+                    val result = snackbarHostState.showSnackbar(
+                        message = event.message,
+                        actionLabel = event.actionLabel,
+                        withDismissAction = true
+                    )
+                    if (result == SnackbarResult.ActionPerformed && event.url != null) {
+                        clipboardManager.setClipEntry(
+                            ClipEntry(
+                                ClipData.newPlainText(
+                                    "url",
+                                    event.url
+                                )
+                            )
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    val importLauncher = rememberLauncherForActivityResult(
+        contract = ActivityResultContracts.OpenDocument(),
+        onResult = { uri ->
+            uri?.let {
+                val groupId = uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
+                viewModel.importBookshelf(it, groupId)
+            }
+        }
+    )
+
+    val exportLauncher = rememberLauncherForActivityResult(
+        contract = ActivityResultContracts.CreateDocument("application/json"),
+        onResult = { uri ->
+            uri?.let { viewModel.exportToUri(it, uiState.items) }
+        }
+    )
+
+    val activeOverlay = uiState.activeOverlay
+    val showGroupMenu = activeOverlay == BookshelfOverlay.GroupMenu
+    val isEditMode = uiState.isEditMode
+    val selectedBookUrls = uiState.selectedBookUrls
+    val isInFolderRoot = uiState.isInFolderRoot
+    val bookGroupStyle = uiState.bookGroupStyle
+
+    val transitionState = remember { SeekableTransitionState(isInFolderRoot) }
+    val folderTransition = rememberTransition(transitionState, label = "FolderTransition")
+    LaunchedEffect(isInFolderRoot) {
+        if (transitionState.targetState != isInFolderRoot) {
+            transitionState.animateTo(isInFolderRoot)
+        }
+    }
+
+    val pagerState = rememberPagerState(
+        initialPage = uiState.selectedGroupIndex.coerceAtLeast(0),
+        pageCount = { uiState.groups.size }
+    )
+    val latestGroups by rememberUpdatedState(uiState.groups)
+    val latestSelectedGroupId by rememberUpdatedState(uiState.selectedGroupId)
+
+    LaunchedEffect(uiState.selectedGroupIndex, uiState.groups.size) {
+        if (uiState.groups.isNotEmpty()
+            && uiState.selectedGroupIndex in uiState.groups.indices
+            && pagerState.currentPage != uiState.selectedGroupIndex
+        ) {
+            pagerState.scrollToPage(uiState.selectedGroupIndex)
+        }
+    }
+
+    LaunchedEffect(pagerState) {
+        snapshotFlow { pagerState.settledPage }
+            .distinctUntilChanged()
+            .collect { page ->
+                val groups = latestGroups
+                if (groups.isNotEmpty() && page in groups.indices) {
+                    val targetGroupId = groups[page].groupId
+                    if (latestSelectedGroupId != targetGroupId) {
+                        viewModel.changeGroup(targetGroupId)
+                    }
+                }
+            }
+    }
+
+    val currentTabGroupId by remember {
+        derivedStateOf {
+            uiState.groups.getOrNull(pagerState.settledPage)?.groupId ?: BookGroup.IdAll
+        }
+    }
+    val searchGroupExists by remember {
+        derivedStateOf { uiState.allGroups.any { it.groupId == uiState.selectedGroupId } }
+    }
+    val currentGroupId by remember {
+        derivedStateOf {
+            if (uiState.isSearch && searchGroupExists) {
+                uiState.selectedGroupId
+            } else {
+                currentTabGroupId
+            }
+        }
+    }
+    val isUsingStandaloneSearchGroup by remember {
+        derivedStateOf {
+            uiState.isSearch && uiState.groups.none { it.groupId == currentGroupId }
+        }
+    }
+    val currentGroupBookCount by remember { derivedStateOf { uiState.currentGroupBookCount } }
+
+    val clearSelection = {
+        viewModel.clearSelection()
+    }
+    val exitEditMode = {
+        viewModel.exitEditMode()
+    }
+    val toggleEditMode = {
+        viewModel.toggleEditMode()
+    }
+    val toggleBookSelection: (String) -> Unit = { bookUrl ->
+        viewModel.toggleBookSelection(bookUrl)
+    }
+
+    LaunchedEffect(pagerState.currentPage, isInFolderRoot) {
+        clearSelection()
+    }
+
+    BackHandler(enabled = isEditMode) {
+        if (selectedBookUrls.isNotEmpty()) {
+            clearSelection()
+        } else {
+            exitEditMode()
+        }
+    }
+
+    val currentGroupName by remember { derivedStateOf { uiState.currentGroupName } }
+
+    PredictiveBackHandler(enabled = bookGroupStyle == 2 && !isInFolderRoot && !isEditMode) { progress ->
+        try {
+            progress.collect { backEvent ->
+                transitionState.seekTo(backEvent.progress, targetState = true)
+            }
+            viewModel.setInFolderRoot(true)
+            transitionState.animateTo(true)
+        } catch (e: CancellationException) {
+            transitionState.animateTo(false)
+        }
+    }
+
+    val configuration = LocalConfiguration.current
+    val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+    val bookshelfFolderLayoutMode by remember(isLandscape) {
+        derivedStateOf {
+            if (isLandscape) BookshelfConfig.bookshelfFolderLayoutModeLandscapeState.value
+            else BookshelfConfig.bookshelfFolderLayoutModePortraitState.value
+        }
+    }
+    val bookshelfFolderLayoutGrid by remember(isLandscape) {
+        derivedStateOf {
+            if (isLandscape) BookshelfConfig.bookshelfFolderLayoutGridLandscapeState.value
+            else BookshelfConfig.bookshelfFolderLayoutGridPortraitState.value
+        }
+    }
+    val bookshelfFolderLayoutList by remember(isLandscape) {
+        derivedStateOf {
+            if (isLandscape) BookshelfConfig.bookshelfFolderLayoutListLandscapeState.value
+            else BookshelfConfig.bookshelfFolderLayoutListPortraitState.value
+        }
+    }
+    val currentMenuGroupId by remember {
+        derivedStateOf { if (uiState.isSearch) uiState.selectedGroupId else currentTabGroupId }
+    }
+    val editStickySummary by remember {
+        derivedStateOf {
+            if (uiState.isEditMode) {
+                BookshelfEditStickySummary(
+                    selectedCount = uiState.selectedBookUrls.size,
+                    currentGroupTotalCount = currentGroupBookCount,
+                    groupName = currentGroupName,
+                    showGroupName = uiState.bookGroupStyle != 0
+                )
+            } else {
+                null
+            }
+        }
+    }
+
+    val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
+    var showTopBarMenu by remember { mutableStateOf(false) }
+    val onSearchClick = {
+        if (BookshelfConfig.bookshelfSearchActionDirectToSearchState.value) {
+            onNavigateToSearch(uiState.searchKey.trim())
+        } else {
+            val active = !uiState.isSearch
+            viewModel.setSearchMode(active)
+            if (!active && uiState.selectedGroupId != currentTabGroupId) {
+                viewModel.changeGroup(currentTabGroupId)
+            }
+        }
+    }
+
+    AppScaffold(
+        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+        contentWindowInsets = WindowInsets(0),
+        snackbarHost = {
+            SnackbarHost(
+                hostState = snackbarHostState,
+                modifier = Modifier
+                    .padding(
+                        bottom = 72.dp + WindowInsets.navigationBars
+                            .asPaddingValues()
+                            .calculateBottomPadding()
+                    )
+            )
+        },
+        topBar = {
+            BookshelfTopBar(
+                uiState = uiState,
+                scrollBehavior = scrollBehavior,
+                onSearchClick = onSearchClick,
+                onSearchQueryChange = { viewModel.setSearchKey(it) },
+                onSearchSubmit = { rawQuery ->
+                    rawQuery.trim()
+                        .takeIf { it.isNotEmpty() }
+                        ?.let(onNavigateToSearch)
+                },
+                onClearSearch = { viewModel.setSearchKey("") },
+                actions = {
+                    AnimatedVisibility(visible = isEditMode) {
+                        TopBarActionButton(
+                            onClick = { viewModel.selectAllVisible() },
+                            imageVector = Icons.Default.SelectAll,
+                            contentDescription = stringResource(R.string.select_all)
+                        )
+                    }
+                    AnimatedVisibility(visible = isEditMode) {
+                        TopBarActionButton(
+                            onClick = { viewModel.invertVisibleSelection() },
+                            imageVector = Icons.Default.Refresh,
+                            contentDescription = stringResource(R.string.revert_selection)
+                        )
+                    }
+                    AnimatedVisibility(visible = isEditMode) {
+                        TopBarActionButton(
+                            onClick = {
+                                if (selectedBookUrls.isNotEmpty()) {
+                                    viewModel.showOverlay(BookshelfOverlay.BatchDownloadConfirmDialog)
+                                }
+                            },
+                            imageVector = Icons.Default.Download,
+                            contentDescription = stringResource(R.string.action_download)
+                        )
+                    }
+                    AnimatedVisibility(visible = isEditMode) {
+                        TopBarActionButton(
+                            onClick = {
+                                if (selectedBookUrls.isNotEmpty()) {
+                                    viewModel.showOverlay(BookshelfOverlay.GroupSelectSheet)
+                                }
+                            },
+                            imageVector = Icons.Default.Bookmarks,
+                            contentDescription = stringResource(R.string.move_to_group)
+                        )
+                    }
+
+                    if (!isEditMode) {
+                        Box {
+                            TopBarActionButton(
+                                onClick = { showTopBarMenu = true },
+                                imageVector = Icons.Default.MoreVert,
+                                contentDescription = stringResource(R.string.more_menu)
+                            )
+                            RoundDropdownMenu(
+                                expanded = showTopBarMenu,
+                                onDismissRequest = { showTopBarMenu = false }
+                            ) { dismiss ->
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.add_remote_book),
+                                    onClick = { onNavigateToRemoteImport(); dismiss() },
+                                    leadingIcon = { Icon(Icons.Default.Wifi, null) }
+                                )
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.book_local),
+                                    onClick = { onNavigateToLocalImport(); dismiss() },
+                                    leadingIcon = { Icon(Icons.Default.Save, null) }
+                                )
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.update_toc),
+                                    onClick = { viewModel.upToc(uiState.items); dismiss() },
+                                    leadingIcon = { Icon(Icons.Default.Refresh, null) }
+                                )
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.layout_setting),
+                                    onClick = {
+                                        viewModel.showOverlay(BookshelfOverlay.ConfigSheet)
+                                        dismiss()
+                                    },
+                                    leadingIcon = { Icon(Icons.Default.GridView, null) }
+                                )
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.group_manage),
+                                    onClick = {
+                                        viewModel.showOverlay(BookshelfOverlay.GroupManageSheet)
+                                        dismiss()
+                                    },
+                                    leadingIcon = { Icon(Icons.Outlined.ViewCarousel, null) }
+                                )
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.add_url),
+                                    onClick = {
+                                        viewModel.showOverlay(BookshelfOverlay.AddUrlDialog)
+                                        dismiss()
+                                    },
+                                    leadingIcon = { Icon(Icons.Default.Link, null) }
+                                )
+                                RoundDropdownMenuItem(
+                                    text = "选择模式",
+                                    onClick = {
+                                        toggleEditMode()
+                                        dismiss()
+                                    },
+                                    leadingIcon = { Icon(Icons.Default.Edit, null) }
+                                )
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.bookshelf_management),
+                                    onClick = {
+                                        val groupId =
+                                            uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId
+                                                ?: -1L
+                                        onNavigateToCache(groupId)
+                                        dismiss()
+                                    },
+                                    leadingIcon = { Icon(Icons.Default.Bookmarks, null) }
+                                )
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.export_bookshelf),
+                                    onClick = {
+                                        viewModel.showOverlay(BookshelfOverlay.ExportSheet)
+                                        dismiss()
+                                    },
+                                    leadingIcon = { Icon(Icons.Default.UploadFile, null) }
+                                )
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.import_bookshelf),
+                                    onClick = {
+                                        viewModel.showOverlay(BookshelfOverlay.ImportSheet)
+                                        dismiss()
+                                    },
+                                    leadingIcon = { Icon(Icons.Default.CloudDownload, null) }
+                                )
+                                RoundDropdownMenuItem(
+                                    text = stringResource(R.string.log),
+                                    onClick = {
+                                        viewModel.showOverlay(BookshelfOverlay.LogSheet)
+                                        dismiss()
+                                    },
+                                    leadingIcon = { Icon(Icons.Default.History, null) }
+                                )
+                            }
+                        }
+                    }
+                },
+                bottomContent = {
+                    if (bookGroupStyle == 0 && uiState.groups.isNotEmpty()) {
+                        Row(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .adaptiveHorizontalPaddingTab(),
+                            verticalAlignment = Alignment.CenterVertically
+                        ) {
+                            val selectedTabIndex =
+                                pagerState.currentPage.coerceIn(0, uiState.groups.size - 1)
+                            val tabTitles = remember(uiState.groups) {
+                                uiState.groups.map { it.groupName }
+                            }
+
+                            AppTabRow(
+                                tabTitles = tabTitles,
+                                selectedTabIndex = selectedTabIndex,
+                                onTabSelected = { index ->
+                                    scope.launch { pagerState.animateScrollToPage(index) }
+                                },
+                                modifier = Modifier.weight(1f)
+                            )
+
+                            val showExpandButton by BookshelfConfig.shouldShowExpandButtonState
+                            if (showExpandButton) {
+                                Box(modifier = Modifier) {
+                                    SmallToggleButton(
+                                        checked = showGroupMenu,
+                                        onCheckedChange = {
+                                            if (it) {
+                                                viewModel.showOverlay(BookshelfOverlay.GroupMenu)
+                                            } else {
+                                                viewModel.dismissOverlay()
+                                            }
+                                        },
+                                        style = ToggleStyle.Outlined,
+                                        icon = Icons.AutoMirrored.Filled.FormatListBulleted,
+                                        contentDescription = stringResource(R.string.group_manage)
+                                    )
+                                    RoundDropdownMenu(
+                                        expanded = showGroupMenu,
+                                        onDismissRequest = { viewModel.dismissOverlay() }
+                                    ) { dismiss ->
+                                        uiState.groups.forEachIndexed { index, group ->
+                                            RoundDropdownMenuItem(
+                                                text = group.groupName,
+                                                onClick = {
+                                                    if (uiState.isSearch) {
+                                                        viewModel.changeGroup(group.groupId)
+                                                    }
+                                                    scope.launch {
+                                                        pagerState.animateScrollToPage(
+                                                            index
+                                                        )
+                                                    }
+                                                    dismiss()
+                                                },
+                                                trailingIcon = {
+                                                    val isSelected = if (uiState.isSearch) {
+                                                        uiState.selectedGroupId == group.groupId
+                                                    } else {
+                                                        selectedTabIndex == index
+                                                    }
+                                                    if (isSelected) {
+                                                        Icon(
+                                                            Icons.Default.Check,
+                                                            null,
+                                                            modifier = Modifier.size(18.dp)
+                                                        )
+                                                    }
+                                                }
+                                            )
+                                        }
+
+                                        if (uiState.isSearch) {
+                                            val allGroup = uiState.allGroups.firstOrNull {
+                                                it.groupId == BookGroup.IdAll
+                                            }
+                                            val hiddenGroups = uiState.allGroups.filter {
+                                                !it.show && it.groupId != BookGroup.IdAll
+                                            }
+
+                                            if (allGroup != null || hiddenGroups.isNotEmpty()) {
+                                                PillHeaderDivider(
+                                                    title = "${stringResource(R.string.all)} / ${
+                                                        stringResource(
+                                                            R.string.hide
+                                                        )
+                                                    }"
+                                                )
+
+                                                allGroup?.let { group ->
+                                                    RoundDropdownMenuItem(
+                                                        text = group.groupName,
+                                                        onClick = {
+                                                            viewModel.changeGroup(group.groupId)
+                                                            dismiss()
+                                                        },
+                                                        trailingIcon = {
+                                                            if (uiState.selectedGroupId == group.groupId) {
+                                                                Icon(
+                                                                    Icons.Default.Check,
+                                                                    null,
+                                                                    modifier = Modifier.size(18.dp)
+                                                                )
+                                                            }
+                                                        }
+                                                    )
+                                                }
+
+                                                hiddenGroups.forEach { group ->
+                                                    RoundDropdownMenuItem(
+                                                        text = group.groupName,
+                                                        onClick = {
+                                                            viewModel.changeGroup(group.groupId)
+                                                            dismiss()
+                                                        },
+                                                        trailingIcon = {
+                                                            if (uiState.selectedGroupId == group.groupId) {
+                                                                Icon(
+                                                                    Icons.Default.Check,
+                                                                    null,
+                                                                    modifier = Modifier.size(18.dp)
+                                                                )
+                                                            }
+                                                        }
+                                                    )
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            )
+        }
+    ) { paddingValues ->
+        val currentGroup by remember {
+            derivedStateOf {
+                if (uiState.isSearch) {
+                    uiState.allGroups.firstOrNull { it.groupId == currentGroupId }
+                } else {
+                    uiState.groups.getOrNull(pagerState.settledPage)
+                }
+            }
+        }
+        val pullToRefreshEnabled by remember {
+            derivedStateOf { (currentGroup?.enableRefresh ?: true) && !isEditMode }
+        }
+
+        Box(Modifier.fillMaxSize()) {
+            AppPullToRefresh(
+                isRefreshing = uiState.isRefreshing,
+                onRefresh = { viewModel.refreshBooks(uiState.items) },
+                enabled = pullToRefreshEnabled,
+                topPadding = paddingValues.calculateTopPadding()
+            ) {
+                folderTransition.AnimatedContent(
+                transitionSpec = {
+                    val easing = FastOutSlowInEasing
+                    val duration = 480
+                    if (targetState) {
+                        (fadeIn(animationSpec = tween(duration, easing = easing)) +
+                                scaleIn(
+                                    initialScale = 1.2f,
+                                    animationSpec = tween(duration, easing = easing)
+                                ))
+                            .togetherWith(fadeOut(animationSpec = tween(duration, easing = easing)) +
+                                    scaleOut(
+                                        targetScale = 0.8f,
+                                        animationSpec = tween(duration, easing = easing)
+                                    )
+                            )
+                    } else {
+                        (fadeIn(animationSpec = tween(duration, easing = easing)) +
+                                scaleIn(
+                                    initialScale = 0.8f,
+                                    animationSpec = tween(duration, easing = easing)
+                                ))
+                            .togetherWith(fadeOut(animationSpec = tween(duration, easing = easing)) +
+                                    scaleOut(
+                                        targetScale = 1.2f,
+                                        animationSpec = tween(duration, easing = easing)
+                                    )
+                            )
+                    }
+                }
+            ) { isRoot ->
+                if (uiState.groups.isEmpty() && !uiState.isSearch) {
+                    if (!uiState.isInitialLoading) {
+                        EmptyMessage(
+                            modifier = Modifier
+                                .fillMaxSize()
+                                .padding(
+                                    top = paddingValues.calculateTopPadding(),
+                                    bottom = 120.dp
+                                ),
+                            messageResId = R.string.bookshelf_empty
+                        )
+                    }
+                } else if (bookGroupStyle == 2 && isRoot && !isUsingStandaloneSearchGroup) {
+                    val folderColumns =
+                        if (bookshelfFolderLayoutMode == 0) bookshelfFolderLayoutList else bookshelfFolderLayoutGrid
+                    val isGridMode = bookshelfFolderLayoutMode != 0
+                    FastScrollLazyVerticalGrid(
+                        columns = GridCells.Fixed(folderColumns.coerceAtLeast(1)),
+                        modifier = Modifier
+                            .fillMaxSize()
+                            .then(
+                                with(sharedTransitionScope) {
+                                    if (this != null) Modifier.skipToLookaheadSize() else Modifier
+                                }
+                            ),
+                        contentPadding = adaptiveContentPaddingBookshelf(
+                            top = paddingValues.calculateTopPadding(),
+                            bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp,
+                            horizontal = 4.dp
+                        ),
+                        verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
+                        horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
+                        showFastScroll = BookshelfConfig.showBookshelfFastScrollerState.value
+                    ) {
+                        itemsIndexed(
+                            uiState.groups,
+                            key = { _, it -> it.groupId }) { index, group ->
+                            val countText = if (BookshelfConfig.showBookCountState.value) {
+                                uiState.groupBookCounts[group.groupId]?.let {
+                                    stringResource(R.string.book_count, it)
+                                }
+                            } else {
+                                null
+                            }
+                            if (bookshelfFolderLayoutMode == 0) {
+                                BookGroupItemList(
+                                    group = group,
+                                    previewBooks = uiState.groupPreviews[group.groupId]
+                                        ?: emptyList(),
+                                    countText = countText,
+                                    isCompact = BookshelfConfig.bookshelfLayoutCompactState.value,
+                                    titleSmallFont = BookshelfConfig.bookshelfTitleSmallFontState.value,
+                                    titleCenter = BookshelfConfig.bookshelfTitleCenterState.value,
+                                    titleMaxLines = BookshelfConfig.bookshelfTitleMaxLinesState.value,
+                                    coverShadow = BookshelfConfig.bookshelfCoverShadowState.value,
+                                    onClick = {
+                                        scope.launch { pagerState.scrollToPage(index) }
+                                        viewModel.setInFolderRoot(false)
+                                    },
+                                    onLongClick = {
+                                        viewModel.showOverlay(BookshelfOverlay.GroupEditSheet(group.groupId))
+                                    },
+                                    onBookClick = onBookClick
+                                )
+                            } else {
+                                BookGroupItemGrid(
+                                    group = group,
+                                    previewBooks = uiState.groupPreviews[group.groupId]
+                                        ?: emptyList(),
+                                    countText = countText,
+                                    gridStyle = BookshelfConfig.bookshelfGridLayoutState.value,
+                                    titleSmallFont = BookshelfConfig.bookshelfTitleSmallFontState.value,
+                                    titleCenter = BookshelfConfig.bookshelfTitleCenterState.value,
+                                    titleMaxLines = BookshelfConfig.bookshelfTitleMaxLinesState.value,
+                                    coverShadow = BookshelfConfig.bookshelfCoverShadowState.value,
+                                    onClick = {
+                                        scope.launch { pagerState.scrollToPage(index) }
+                                        viewModel.setInFolderRoot(false)
+                                    },
+                                    onLongClick = {
+                                        viewModel.showOverlay(BookshelfOverlay.GroupEditSheet(group.groupId))
+                                    }
+                                )
+                            }
+                        }
+                    }
+                } else {
+                    if (isUsingStandaloneSearchGroup) {
+                        BookshelfPage(
+                            paddingValues = paddingValues,
+                            books = uiState.items,
+                            uiState = uiState,
+                            selectedBookUrls = selectedBookUrls,
+                            canReorderBooks = false,
+                            onToggleBookSelection = { toggleBookSelection(it.book.bookUrl) },
+                            draggingBooks = null,
+                            pendingSavedBooks = null,
+                            onDragStarted = {},
+                            onMoveBook = { _, _, _ -> },
+                            onDragFinished = {},
+                            onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
+                            onBookClick = onBookClick,
+                            onBookLongClick = onBookLongClick,
+                            isCurrentPage = true,
+                            sharedTransitionScope = sharedTransitionScope,
+                            animatedVisibilityScope = animatedVisibilityScope,
+                        )
+                    } else {
+                        HorizontalPager(
+                            state = pagerState,
+                            modifier = Modifier
+                                .fillMaxSize()
+                                .then(
+                                    with(sharedTransitionScope) {
+                                        if (this != null) Modifier.skipToLookaheadSize() else Modifier
+                                    }
+                                ),
+                            beyondViewportPageCount = 0,
+                            key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
+                        ) { pageIndex ->
+                            val group = uiState.groups.getOrNull(pageIndex)
+                            if (group != null) {
+                                val isSelectedGroup = group.groupId == uiState.selectedGroupId
+                                val books = uiState.allGroupBooks[group.groupId]
+                                    ?: if (isSelectedGroup) uiState.items
+                                    else persistentListOf()
+                                val canReorderBooks = isEditMode &&
+                                        !uiState.isSearch &&
+                                        (group.bookSort.takeIf { it >= 0 }
+                                            ?: uiState.bookshelfSort) == 3 &&
+                                        isSelectedGroup
+                                BookshelfPage(
+                                    paddingValues = paddingValues,
+                                    books = books,
+                                    uiState = uiState,
+                                    selectedBookUrls = selectedBookUrls,
+                                    canReorderBooks = canReorderBooks,
+                                    onToggleBookSelection = { toggleBookSelection(it.book.bookUrl) },
+                                    draggingBooks = if (isSelectedGroup) {
+                                        uiState.draggingBooks
+                                    } else {
+                                        null
+                                    },
+                                    pendingSavedBooks = if (isSelectedGroup) {
+                                        uiState.pendingSavedBooks
+                                    } else {
+                                        null
+                                    },
+                                    onDragStarted = {
+                                        if (isSelectedGroup) viewModel.startDraggingBooks(it)
+                                    },
+                                    onMoveBook = { from, to, currentBooks ->
+                                        if (isSelectedGroup) {
+                                            viewModel.moveDraggingBook(from, to, currentBooks)
+                                        }
+                                    },
+                                    onDragFinished = {
+                                        if (isSelectedGroup) viewModel.finishDraggingBooks()
+                                    },
+                                    onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
+                                    onBookClick = onBookClick,
+                                    onBookLongClick = onBookLongClick,
+                                    isCurrentPage = isSelectedGroup,
+                                    sharedTransitionScope = sharedTransitionScope,
+                                    animatedVisibilityScope = animatedVisibilityScope,
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+            }
+
+            TopFloatingStickyItem(
+                item = editStickySummary,
+                modifier = Modifier
+                    .align(Alignment.TopCenter)
+                    .padding(top = paddingValues.calculateTopPadding() + 6.dp),
+            ) { summary ->
+                Box {
+                    NormalCard(
+                        cornerRadius = 32.dp,
+                        containerColor = LegadoTheme.colorScheme.surfaceContainer,
+                        contentColor = LegadoTheme.colorScheme.onCardContainer
+                    ) {
+                        Row(
+                            modifier = Modifier.padding(all = 8.dp),
+                            verticalAlignment = Alignment.CenterVertically
+                        ) {
+                            TextCard(
+                                icon = AppIcons.Close,
+                                backgroundColor = LegadoTheme.colorScheme.surfaceContainerHighest,
+                                cornerRadius = 16.dp,
+                                verticalPadding = 8.dp,
+                                horizontalPadding = 8.dp,
+                                onClick = exitEditMode
+                            )
+                            Spacer(modifier = Modifier.width(8.dp))
+                            AppText(
+                                text = stringResource(
+                                    R.string.bookshelf_selected_count,
+                                    summary.selectedCount
+                                ),
+                                style = LegadoTheme.typography.labelSmallEmphasized
+                            )
+                            AppText(
+                                text = " · ${
+                                    stringResource(
+                                        R.string.bookshelf_total_count,
+                                        summary.currentGroupTotalCount
+                                    )
+                                }",
+                                style = LegadoTheme.typography.labelSmallEmphasized
+                            )
+                            Spacer(modifier = Modifier.width(8.dp))
+                            if (summary.showGroupName && !summary.groupName.isNullOrBlank()) {
+                                TextCard(
+                                    text = summary.groupName,
+                                    textStyle = LegadoTheme.typography.labelSmallEmphasized,
+                                    backgroundColor = LegadoTheme.colorScheme.surfaceContainerHighest,
+                                    cornerRadius = 16.dp,
+                                    verticalPadding = 8.dp,
+                                    horizontalPadding = 12.dp,
+                                    onClick = {
+                                        viewModel.showOverlay(BookshelfOverlay.GroupMenu)
+                                    }
+                                )
+                            }
+                        }
+                    }
+
+
+                    if (summary.showGroupName) {
+                        RoundDropdownMenu(
+                            expanded = showGroupMenu,
+                            onDismissRequest = { viewModel.dismissOverlay() }
+                        ) { dismiss ->
+                            uiState.groups.forEach { group ->
+                                RoundDropdownMenuItem(
+                                    text = group.groupName,
+                                    onClick = {
+                                        val targetIndex =
+                                            uiState.groups.indexOfFirst { it.groupId == group.groupId }
+                                        if (targetIndex >= 0) {
+                                            scope.launch {
+                                                if (pagerState.currentPage != targetIndex) {
+                                                    pagerState.animateScrollToPage(targetIndex)
+                                                }
+                                            }
+                                        }
+                                        if (uiState.isSearch || uiState.selectedGroupId != group.groupId) {
+                                            viewModel.changeGroup(group.groupId)
+                                        }
+                                        if (bookGroupStyle == 2) {
+                                            viewModel.setInFolderRoot(false)
+                                        }
+                                        dismiss()
+                                    },
+                                    trailingIcon = {
+                                        if (currentMenuGroupId == group.groupId) {
+                                            Icon(
+                                                Icons.Default.Check,
+                                                null,
+                                                modifier = Modifier.size(18.dp)
+                                            )
+                                        }
+                                    }
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+
+        }
+    }
+
+    BookshelfOverlays(
+        activeOverlay = activeOverlay,
+        uiState = uiState,
+        viewModel = viewModel,
+        selectedBookUrls = selectedBookUrls,
+        importLauncher = importLauncher,
+        exportLauncher = exportLauncher,
+        clearSelection = clearSelection
+    )
+}
+
+@Composable
+private fun BookshelfTopBar(
+    uiState: BookshelfUiState,
+    scrollBehavior: GlassTopAppBarScrollBehavior,
+    onSearchClick: () -> Unit,
+    onSearchQueryChange: (String) -> Unit,
+    onSearchSubmit: (String) -> Unit,
+    onClearSearch: () -> Unit,
+    actions: @Composable RowScope.() -> Unit = {},
+    bottomContent: @Composable ColumnScope.() -> Unit = {}
+) {
+    val searchContentDescription = stringResource(R.string.search)
+    GlassMediumFlexibleTopAppBar(
+        modifier = Modifier.fillMaxWidth(),
+        title = if (uiState.isLoading) {
+            stringResource(R.string.loading)
+        } else {
+            uiState.title.ifEmpty { stringResource(R.string.bookshelf) }
+        },
+        useCharMode = uiState.isLoading,
+        subtitle = uiState.subtitle,
+        scrollBehavior = scrollBehavior,
+        actions = {
+            TopBarActionButton(
+                onClick = onSearchClick,
+                imageVector = AppIcons.Search,
+                contentDescription = searchContentDescription
+            )
+            actions()
+        },
+        bottomContent = {
+            AnimatedVisibility(
+                modifier = Modifier.adaptiveHorizontalPadding(),
+                visible = uiState.isSearch,
+                enter = expandVertically() + fadeIn(),
+                exit = shrinkVertically() + fadeOut()
+            ) {
+                SearchBar(
+                    query = uiState.searchKey,
+                    onQueryChange = onSearchQueryChange,
+                    onSearch = onSearchSubmit,
+                    placeholder = stringResource(R.string.input_search_key),
+                    leadingIcon = {
+                        AppIcon(
+                            imageVector = AppIcons.Search,
+                            contentDescription = null
+                        )
+                    },
+                    trailingIcon = {
+                        if (uiState.searchKey.isNotEmpty()) {
+                            SmallPlainButton(
+                                modifier = Modifier.padding(start = 12.dp),
+                                onClick = onClearSearch,
+                                icon = AppIcons.Close,
+                                contentDescription = stringResource(R.string.clear)
+                            )
+                        }
+                    }
+                )
+            }
+            bottomContent()
+        }
+    )
+}
+
+@Composable
+private fun BookshelfOverlays(
+    activeOverlay: BookshelfOverlay?,
+    uiState: BookshelfUiState,
+    viewModel: BookshelfViewModel,
+    selectedBookUrls: Set<String>,
+    importLauncher: ManagedActivityResultLauncher<Array<String>, Uri?>,
+    exportLauncher: ManagedActivityResultLauncher<String, Uri?>,
+    clearSelection: () -> Unit
+) {
+    BookshelfConfigSheet(
+        show = activeOverlay == BookshelfOverlay.ConfigSheet,
+        onDismissRequest = { viewModel.dismissOverlay() }
+    )
+
+    GroupManageSheet(
+        show = activeOverlay == BookshelfOverlay.GroupManageSheet,
+        onDismissRequest = { viewModel.dismissOverlay() }
+    )
+
+    val groups by viewModel.allGroupsFlow.collectAsStateWithLifecycle()
+
+    if (activeOverlay is BookshelfOverlay.GroupEditSheet) {
+        val editGroup = groups.firstOrNull { it.groupId == activeOverlay.groupId }
+        if (editGroup != null) {
+            GroupEditSheet(
+                show = true,
+                group = editGroup,
+                onDismissRequest = { viewModel.dismissOverlay() }
+            )
+        }
+    }
+
+    GroupSelectSheet(
+        show = activeOverlay == BookshelfOverlay.GroupSelectSheet,
+        groups = groups.filter { it.groupId > 0 },
+        currentGroupId = 0L,
+        onDismissRequest = { viewModel.dismissOverlay() },
+        onConfirm = { groupId ->
+            viewModel.moveBooksToGroup(selectedBookUrls, groupId)
+            viewModel.dismissOverlay()
+            clearSelection()
+        }
+    )
+
+    SourceInputDialog(
+        show = activeOverlay == BookshelfOverlay.AddUrlDialog,
+        title = stringResource(R.string.add_book_url),
+        onDismissRequest = { viewModel.dismissOverlay() },
+        onConfirm = { url ->
+            viewModel.addBookByUrl(url)
+            viewModel.dismissOverlay()
+        }
+    )
+
+    FilePickerSheet(
+        show = activeOverlay == BookshelfOverlay.ImportSheet,
+        onDismissRequest = { viewModel.dismissOverlay() },
+        title = stringResource(R.string.import_bookshelf),
+        onSelectSysFile = { types ->
+            importLauncher.launch(types)
+            viewModel.dismissOverlay()
+        },
+        onManualInput = {
+            viewModel.showOverlay(BookshelfOverlay.AddUrlDialog)
+        },
+        allowExtensions = arrayOf("json", "txt")
+    )
+
+    FilePickerSheet(
+        show = activeOverlay == BookshelfOverlay.ExportSheet,
+        onDismissRequest = { viewModel.dismissOverlay() },
+        title = stringResource(R.string.export_bookshelf),
+        onSelectSysDir = {
+            viewModel.dismissOverlay()
+            exportLauncher.launch("bookshelf.json")
+        },
+        onUpload = {
+            viewModel.dismissOverlay()
+            viewModel.uploadBookshelf(uiState.items)
+        }
+    )
+
+    AppLogSheet(
+        show = activeOverlay == BookshelfOverlay.LogSheet,
+        onDismissRequest = { viewModel.dismissOverlay() }
+    )
+
+    AppAlertDialog(
+        show = activeOverlay == BookshelfOverlay.BatchDownloadConfirmDialog,
+        onDismissRequest = { viewModel.dismissOverlay() },
+        title = stringResource(R.string.draw),
+        text = stringResource(R.string.sure_cache_book),
+        confirmText = stringResource(android.R.string.ok),
+        onConfirm = {
+            viewModel.dismissOverlay()
+            viewModel.downloadBooks(selectedBookUrls)
+        },
+        dismissText = stringResource(android.R.string.cancel),
+        onDismiss = { viewModel.dismissOverlay() }
+    )
+
+    if (uiState.isLoading) {
+        Dialog(onDismissRequest = {}) {
+            NormalCard(
+                cornerRadius = 12.dp,
+                containerColor = LegadoTheme.colorScheme.surfaceContainerHigh
+            ) {
+                Column(
+                    modifier = Modifier.padding(24.dp),
+                    horizontalAlignment = Alignment.CenterHorizontally
+                ) {
+                    AppCircularProgressIndicator()
+                    uiState.loadingText?.let {
+                        AppText(
+                            text = it,
+                            modifier = Modifier.padding(top = 16.dp),
+                            style = LegadoTheme.typography.bodyMedium
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
+
+private data class BookshelfEditStickySummary(
+    val selectedCount: Int,
+    val currentGroupTotalCount: Int,
+    val groupName: String?,
+    val showGroupName: Boolean,
+)
+
+@Composable
+fun BookshelfPage(
+    paddingValues: PaddingValues,
+    books: ImmutableList<BookUiItem>,
+    uiState: BookshelfUiState,
+    selectedBookUrls: ImmutableSet<String>,
+    canReorderBooks: Boolean,
+    onToggleBookSelection: (BookUiItem) -> Unit,
+    draggingBooks: ImmutableList<BookUiItem>?,
+    pendingSavedBooks: ImmutableList<BookUiItem>?,
+    onDragStarted: (ImmutableList<BookUiItem>) -> Unit,
+    onMoveBook: (fromIndex: Int, toIndex: Int, currentBooks: ImmutableList<BookUiItem>) -> Unit,
+    onDragFinished: () -> Unit,
+    onGlobalSearch: () -> Unit,
+    onBookClick: (BookShelfItem) -> Unit,
+    onBookLongClick: (BookShelfItem, String?) -> Unit,
+    isCurrentPage: Boolean = true,
+    sharedTransitionScope: SharedTransitionScope? = null,
+    animatedVisibilityScope: AnimatedVisibilityScope? = null,
+) {
+    if (books.isEmpty()) {
+        if (!isCurrentPage) return
+        if (uiState.isSearch) {
+            EmptyMessage(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(
+                        top = paddingValues.calculateTopPadding(),
+                        bottom = 120.dp
+                    ),
+                message = stringResource(R.string.bookshelf_empty_global_search),
+                buttonText = stringResource(R.string.global_search),
+                onButtonClick = onGlobalSearch
+            )
+        } else if (!uiState.isInitialLoading) {
+            EmptyMessage(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(
+                        top = paddingValues.calculateTopPadding(),
+                        bottom = 120.dp
+                    ),
+                messageResId = R.string.bookshelf_empty
+            )
+        }
+        return
+    }
+
+    val configuration = LocalConfiguration.current
+    val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+    val bookshelfLayoutMode by remember(isLandscape) {
+        derivedStateOf {
+            if (isLandscape) BookshelfConfig.bookshelfLayoutModeLandscapeState.value
+            else BookshelfConfig.bookshelfLayoutModePortraitState.value
+        }
+    }
+    val bookshelfLayoutGrid by remember(isLandscape) {
+        derivedStateOf {
+            if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscapeState.value
+            else BookshelfConfig.bookshelfLayoutGridPortraitState.value
+        }
+    }
+    val bookshelfLayoutList by remember(isLandscape) {
+        derivedStateOf {
+            if (isLandscape) BookshelfConfig.bookshelfLayoutListLandscapeState.value
+            else BookshelfConfig.bookshelfLayoutListPortraitState.value
+        }
+    }
+    val columns by remember {
+        derivedStateOf {
+            if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
+        }
+    }
+    val isGridMode by remember { derivedStateOf { bookshelfLayoutMode != 0 } }
+    val bookItemGridStyle by BookshelfConfig.bookshelfGridLayoutState
+    val bookItemIsCompact by BookshelfConfig.bookshelfLayoutCompactState
+    val bookItemTitleSmallFont by BookshelfConfig.bookshelfTitleSmallFontState
+    val bookItemTitleCenter by BookshelfConfig.bookshelfTitleCenterState
+    val bookItemTitleMaxLines by BookshelfConfig.bookshelfTitleMaxLinesState
+    val bookItemCoverShadow by BookshelfConfig.bookshelfCoverShadowState
+    val showFastScroll by BookshelfConfig.showBookshelfFastScrollerState
+    val totalHorizontalPadding =
+        if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) 12.dp else 16.dp
+    val gridContentHorizontalPadding = totalHorizontalPadding / 2
+    val gridInnerHorizontalPadding = totalHorizontalPadding / 2
+    val hapticFeedback = LocalHapticFeedback.current
+    val displayBooks = draggingBooks ?: pendingSavedBooks ?: books
+    val gridState = rememberLazyGridState()
+    val reorderableState = rememberReorderableLazyGridState(gridState) { from, to ->
+        if (canReorderBooks) {
+            onMoveBook(from.index, to.index, displayBooks)
+            hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
+        }
+    }
+    LaunchedEffect(reorderableState.isAnyItemDragging) {
+        if (!reorderableState.isAnyItemDragging) {
+            onDragFinished()
+        }
+    }
+
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .then(
+                with(sharedTransitionScope) {
+                    if (this != null) Modifier.skipToLookaheadSize() else Modifier
+                }
+            )
+    ) {
+        FastScrollLazyVerticalGrid(
+            columns = GridCells.Fixed(columns.coerceAtLeast(1)),
+            state = gridState,
+            modifier = Modifier
+                .fillMaxSize()
+                .semantics(mergeDescendants = true) { contentDescription = "bookshelf_list" }
+                .then(
+                    with(sharedTransitionScope) {
+                        if (this != null) Modifier.skipToLookaheadSize() else Modifier
+                    }
+                ),
+            contentPadding = adaptiveContentPaddingBookshelf(
+                top = paddingValues.calculateTopPadding(),
+                bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp,
+                horizontal = 8.dp
+            ),
+            verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
+            horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
+            showFastScroll = showFastScroll
+        ) {
+            items(displayBooks, key = { it.book.bookUrl }) { bookUi ->
+                val isSelected = selectedBookUrls.contains(bookUi.book.bookUrl)
+                val sharedCoverKey = if (isCurrentPage) {
+                    bookCoverSharedElementKey(
+                        bookUi.book.bookUrl,
+                        "bookshelf:${uiState.selectedGroupId}"
+                    )
+                } else {
+                    null
+                }
+                ReorderableItem(
+                    state = reorderableState,
+                    key = bookUi.book.bookUrl,
+                    enabled = canReorderBooks
+                ) { isDragging ->
+                    BookItem(
+                        bookUi = bookUi,
+                        modifier = Modifier
+                            .then(
+                                if (canReorderBooks) {
+                                    Modifier.longPressDraggableHandle(
+                                        onDragStarted = {
+                                            onDragStarted(displayBooks)
+                                            hapticFeedback.performHapticFeedback(
+                                                HapticFeedbackType.GestureThresholdActivate
+                                            )
+                                        },
+                                        onDragStopped = {
+                                            hapticFeedback.performHapticFeedback(
+                                                HapticFeedbackType.GestureEnd
+                                            )
+                                        }
+                                    )
+                                } else {
+                                    Modifier
+                                }
+                            )
+                            .alpha(if (isDragging) 0.5f else 1f),
+                        layoutMode = bookshelfLayoutMode,
+                        isSelected = isSelected,
+                        gridStyle = bookItemGridStyle,
+                        isCompact = bookItemIsCompact,
+                        isUpdating = uiState.updatingBooks.contains(bookUi.book.bookUrl),
+                        titleSmallFont = bookItemTitleSmallFont,
+                        titleCenter = bookItemTitleCenter,
+                        titleMaxLines = bookItemTitleMaxLines,
+                        coverShadow = bookItemCoverShadow,
+                        isSearchMode = uiState.isSearch,
+                        searchKey = uiState.searchKey,
+                        sharedTransitionScope = sharedTransitionScope,
+                        animatedVisibilityScope = animatedVisibilityScope,
+                        sharedCoverKey = sharedCoverKey,
+                        onClick = {
+                            if (uiState.isEditMode) {
+                                onToggleBookSelection(bookUi)
+                            } else {
+                                onBookClick(bookUi.book)
+                            }
+                        },
+                        onLongClick = if (canReorderBooks) {
+                            null
+                        } else {
+                            {
+                                if (uiState.isEditMode) {
+                                    onToggleBookSelection(bookUi)
+                                } else {
+                                    onBookLongClick(bookUi.book, sharedCoverKey)
+                                }
+                            }
+                        }
+                    )
+                }
+            }
+        }
+    }
+}