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 bd4c54c54..cf7db99c5 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 @@ -27,6 +27,7 @@ import androidx.compose.animation.scaleOut import androidx.compose.animation.shrinkVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -39,16 +40,19 @@ 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.height 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.LazyGridState 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.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.FormatListBulleted import androidx.compose.material.icons.filled.Bookmarks @@ -84,8 +88,10 @@ 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.input.pointer.pointerInput import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalConfiguration @@ -144,1271 +150,4 @@ 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, - importLauncher: ManagedActivityResultLauncher, Uri?>, - exportLauncher: ManagedActivityResultLauncher, - 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, - uiState: BookshelfUiState, - selectedBookUrls: ImmutableSet, - canReorderBooks: Boolean, - onToggleBookSelection: (BookUiItem) -> Unit, - draggingBooks: ImmutableList?, - pendingSavedBooks: ImmutableList?, - onDragStarted: (ImmutableList) -> Unit, - onMoveBook: (fromIndex: Int, toIndex: Int, currentBooks: ImmutableList) -> 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 - ) { - BookItem( - bookUi = bookUi, - modifier = Modifier.then( - if (canReorderBooks) { - Modifier.longPressDraggableHandle( - onDragStarted = { - onDragStarted(displayBooks) - hapticFeedback.performHapticFeedback( - HapticFeedbackType.GestureThresholdActivate - ) - }, - onDragStopped = { - hapticFeedback.performHapticFeedback( - HapticFeedbackType.GestureEnd - ) - } - ) - } else { - Modifier - } - ), - 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) - } - } - } - ) - } - } - } - } -} +import sh.calvin.reorderable.rememberReorderableLazyGridState \ No newline at end of file