diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt index 3abde4956..e703e1d0e 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt @@ -54,6 +54,7 @@ fun BookshelfItem( cover: @Composable (Modifier) -> Unit, title: String, modifier: Modifier = Modifier, + isSelected: Boolean = false, subTitle: String? = null, desc: String? = null, extra: @Composable (RowScope.() -> Unit)? = null, @@ -64,10 +65,16 @@ fun BookshelfItem( onClick: () -> Unit, onLongClick: () -> Unit ) { + val selectedColor = if (isSelected) { + LegadoTheme.colorScheme.secondaryContainer.copy(alpha = 0.55f) + } else { + Color.Transparent + } if (isGrid) { Box( modifier = modifier .clip(MaterialTheme.shapes.small) + .background(selectedColor) .combinedClickable( onClick = onClick, onLongClick = onLongClick @@ -138,6 +145,8 @@ fun BookshelfItem( Row( modifier = modifier .fillMaxWidth() + .clip(MaterialTheme.shapes.small) + .background(selectedColor) .combinedClickable( onClick = onClick, onLongClick = onLongClick @@ -377,6 +386,7 @@ fun BookItem( book: BookShelfItem, layoutMode: Int, modifier: Modifier = Modifier, + isSelected: Boolean = false, gridStyle: Int = 0, isCompact: Boolean = false, isUpdating: Boolean = false, @@ -404,6 +414,7 @@ fun BookItem( isGrid = layoutMode != 0, gridStyle = gridStyle, isCompact = isCompact, + isSelected = isSelected, modifier = modifier, cover = { modifier -> BookshelfCover( 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 1f02a82d0..00ac9effe 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 @@ -23,6 +23,7 @@ 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.Download import androidx.compose.material.icons.filled.Edit @@ -32,7 +33,7 @@ import androidx.compose.material.icons.filled.ImportExport import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Save -import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material.icons.filled.Wifi import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -67,7 +68,7 @@ import io.legado.app.R import io.legado.app.base.BaseRuleEvent import io.legado.app.ui.about.AppLogSheet import io.legado.app.ui.book.cache.CacheActivity -import io.legado.app.ui.book.manage.BookshelfManageActivity +import io.legado.app.ui.book.info.GroupSelectSheet import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig import io.legado.app.ui.theme.LegadoTheme @@ -77,8 +78,10 @@ import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton +import io.legado.app.ui.widget.components.button.TopBarActionButton import io.legado.app.ui.widget.components.card.NormalCard import io.legado.app.ui.widget.components.filePicker.FilePickerSheet +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.ListScaffold @@ -117,6 +120,9 @@ fun BookshelfScreen( var showGroupManageSheet by remember { mutableStateOf(false) } var showLogSheet by remember { mutableStateOf(false) } var showGroupMenu by remember { mutableStateOf(false) } + var showGroupSelectSheet by remember { mutableStateOf(false) } + var isEditMode by remember { mutableStateOf(false) } + var selectedBookUrls by remember { mutableStateOf>(emptySet()) } val clipboardManager = LocalClipboard.current val snackbarHostState = remember { SnackbarHostState() } @@ -202,6 +208,50 @@ fun BookshelfScreen( val bookGroupStyle = BookshelfConfig.bookGroupStyle // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图 var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) } + + val clearSelection = { + selectedBookUrls = emptySet() + } + val exitEditMode = { + isEditMode = false + clearSelection() + } + val toggleEditMode = { + if (isEditMode) { + exitEditMode() + } else { + if (bookGroupStyle == 2 && isInFolderRoot) { + isInFolderRoot = false + } + isEditMode = true + clearSelection() + } + } + val toggleBookSelection: (String) -> Unit = { bookUrl -> + selectedBookUrls = if (selectedBookUrls.contains(bookUrl)) { + selectedBookUrls - bookUrl + } else { + selectedBookUrls + bookUrl + } + } + + LaunchedEffect(pagerState.currentPage, isInFolderRoot) { + clearSelection() + } + + LaunchedEffect(uiState.items) { + val visibleBookUrls = uiState.items.mapTo(hashSetOf()) { it.bookUrl } + selectedBookUrls = selectedBookUrls.intersect(visibleBookUrls) + } + + BackHandler(enabled = isEditMode) { + if (selectedBookUrls.isNotEmpty()) { + clearSelection() + } else { + exitEditMode() + } + } + val baseTitle = when (bookGroupStyle) { 1 -> { uiState.groups.getOrNull(pagerState.currentPage)?.groupName @@ -221,7 +271,7 @@ fun BookshelfScreen( baseTitle } - if (bookGroupStyle == 2 && !isInFolderRoot) { + if (bookGroupStyle == 2 && !isInFolderRoot && !isEditMode) { BackHandler { isInFolderRoot = true } @@ -243,85 +293,113 @@ fun BookshelfScreen( ListScaffold( title = title, state = uiState, + onBackClick = if (isEditMode) exitEditMode else null, + backNavigationIcon = if (isEditMode) AppIcons.Close else AppIcons.Back, + showSearchAction = !isEditMode, onSearchToggle = { context.startActivity() }, onSearchQueryChange = { viewModel.setSearchKey(it) }, - topBarActions = { }, - dropDownMenuContent = { 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 = { showConfigSheet = true; dismiss() }, - leadingIcon = { Icon(Icons.Default.GridView, null) } - ) - RoundDropdownMenuItem( - text = stringResource(R.string.group_manage), - onClick = { showGroupManageSheet = true; dismiss() }, - leadingIcon = { Icon(Icons.Default.Edit, null) } - ) - RoundDropdownMenuItem( - text = stringResource(R.string.add_url), - onClick = { showAddUrlDialog = true; dismiss() }, - leadingIcon = { Icon(Icons.Default.Link, null) } - ) - RoundDropdownMenuItem( - text = stringResource(R.string.bookshelf_management), - onClick = { - val groupId = - uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L - context.startActivity { - putExtra("groupId", groupId) - } - dismiss() - }, - leadingIcon = { Icon(Icons.Default.Settings, null) } - ) - RoundDropdownMenuItem( - text = stringResource(R.string.cache_export), - onClick = { - val groupId = - uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L - context.startActivity { - putExtra("groupId", groupId) - } - dismiss() - }, - leadingIcon = { Icon(Icons.Default.Download, null) } - ) - RoundDropdownMenuItem( - text = stringResource(R.string.export_bookshelf), - onClick = { - showExportSheet = true - dismiss() - }, - leadingIcon = { Icon(Icons.Default.ImportExport, null) } - ) - RoundDropdownMenuItem( - text = stringResource(R.string.import_bookshelf), - onClick = { showImportSheet = true; dismiss() } - ) - RoundDropdownMenuItem( - text = stringResource(R.string.log), - onClick = { - showLogSheet = true - dismiss() - }, - leadingIcon = { Icon(Icons.Default.History, null) } - ) + topBarActions = { + if (isEditMode) { + TopBarActionButton( + onClick = { + selectedBookUrls = uiState.items.mapTo(hashSetOf()) { it.bookUrl } + }, + imageVector = Icons.Default.SelectAll, + contentDescription = stringResource(R.string.select_all) + ) + TopBarActionButton( + onClick = { + val visibleBookUrls = uiState.items.mapTo(hashSetOf()) { it.bookUrl } + selectedBookUrls = visibleBookUrls - selectedBookUrls + }, + imageVector = Icons.Default.Refresh, + contentDescription = stringResource(R.string.revert_selection) + ) + TopBarActionButton( + onClick = { + if (selectedBookUrls.isNotEmpty()) { + showGroupSelectSheet = true + } + }, + imageVector = Icons.Default.Bookmarks, + contentDescription = stringResource(R.string.move_to_group) + ) + } }, + dropDownMenuContent = if (!isEditMode) { + { 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 = { showConfigSheet = true; dismiss() }, + leadingIcon = { Icon(Icons.Default.GridView, null) } + ) + RoundDropdownMenuItem( + text = stringResource(R.string.group_manage), + onClick = { showGroupManageSheet = true; dismiss() }, + leadingIcon = { Icon(Icons.Default.Edit, null) } + ) + RoundDropdownMenuItem( + text = stringResource(R.string.add_url), + onClick = { showAddUrlDialog = true; dismiss() }, + leadingIcon = { Icon(Icons.Default.Link, null) } + ) + RoundDropdownMenuItem( + text = stringResource(R.string.edit), + onClick = { + toggleEditMode() + dismiss() + }, + leadingIcon = { Icon(Icons.Default.Edit, null) } + ) + RoundDropdownMenuItem( + text = stringResource(R.string.cache_export), + onClick = { + val groupId = + uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L + context.startActivity { + putExtra("groupId", groupId) + } + dismiss() + }, + leadingIcon = { Icon(Icons.Default.Download, null) } + ) + RoundDropdownMenuItem( + text = stringResource(R.string.export_bookshelf), + onClick = { + showExportSheet = true + dismiss() + }, + leadingIcon = { Icon(Icons.Default.ImportExport, null) } + ) + RoundDropdownMenuItem( + text = stringResource(R.string.import_bookshelf), + onClick = { showImportSheet = true; dismiss() } + ) + RoundDropdownMenuItem( + text = stringResource(R.string.log), + onClick = { + showLogSheet = true + dismiss() + }, + leadingIcon = { Icon(Icons.Default.History, null) } + ) + } + } else null, snackbarHostState = snackbarHostState, bottomContent = if (bookGroupStyle == 0) { { @@ -388,7 +466,7 @@ fun BookshelfScreen( var isRefreshing by remember { mutableStateOf(false) } val pullToRefreshState = rememberPullToRefreshState() val currentGroup = uiState.groups.getOrNull(pagerState.currentPage) - val pullToRefreshEnabled = currentGroup?.enableRefresh ?: true + val pullToRefreshEnabled = (currentGroup?.enableRefresh ?: true) && !isEditMode Box( modifier = Modifier @@ -495,6 +573,9 @@ fun BookshelfScreen( bookshelfLayoutMode = bookshelfLayoutMode, bookshelfLayoutGrid = bookshelfLayoutGrid, bookshelfLayoutList = bookshelfLayoutList, + isEditMode = isEditMode, + selectedBookUrls = selectedBookUrls, + onToggleBookSelection = { toggleBookSelection(it.bookUrl) }, onBookClick = onBookClick, onBookLongClick = onBookLongClick ) @@ -523,6 +604,17 @@ fun BookshelfScreen( onDismissRequest = { showGroupManageSheet = false } ) + GroupSelectSheet( + show = showGroupSelectSheet, + currentGroupId = 0L, + onDismissRequest = { showGroupSelectSheet = false }, + onConfirm = { groupId -> + viewModel.moveBooksToGroup(selectedBookUrls, groupId) + showGroupSelectSheet = false + clearSelection() + } + ) + SourceInputDialog( show = showAddUrlDialog, title = stringResource(R.string.add_book_url), @@ -599,6 +691,9 @@ fun BookshelfPage( bookshelfLayoutMode: Int, bookshelfLayoutGrid: Int, bookshelfLayoutList: Int, + isEditMode: Boolean, + selectedBookUrls: Set, + onToggleBookSelection: (BookShelfItem) -> Unit, onBookClick: (BookShelfItem) -> Unit, onBookLongClick: (BookShelfItem) -> Unit ) { @@ -636,10 +731,12 @@ fun BookshelfPage( showFastScroll = BookshelfConfig.showBookshelfFastScroller ) { items(books, key = { it.bookUrl }) { book -> + val isSelected = selectedBookUrls.contains(book.bookUrl) BookItem( book = book, modifier = Modifier.animateItem(), layoutMode = bookshelfLayoutMode, + isSelected = isSelected, gridStyle = BookshelfConfig.bookshelfGridLayout, isCompact = BookshelfConfig.bookshelfLayoutCompact, isUpdating = uiState.updatingBooks.contains(book.bookUrl), @@ -647,10 +744,21 @@ fun BookshelfPage( titleCenter = BookshelfConfig.bookshelfTitleCenter, titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines, coverShadow = BookshelfConfig.bookshelfCoverShadow, - onClick = { onBookClick(book) }, - onLongClick = { onBookLongClick(book) } + onClick = { + if (isEditMode) { + onToggleBookSelection(book) + } else { + onBookClick(book) + } + }, + onLongClick = { + if (isEditMode) { + onToggleBookSelection(book) + } else { + onBookLongClick(book) + } + } ) } } } - diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index 5efe49baa..9513a9eb3 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -363,6 +363,34 @@ class BookshelfViewModel( refreshTrigger.value++ } + fun moveBooksToGroup(bookUrls: Set, groupId: Long) { + updateBooksGroup(bookUrls) { groupId } + } + + private fun updateBooksGroup( + bookUrls: Set, + transform: (Long) -> Long + ) { + if (bookUrls.isEmpty()) return + execute { + val updateBooks = bookUrls.mapNotNull { url -> + appDb.bookDao.getBook(url)?.let { book -> + val targetGroup = transform(book.group) + if (targetGroup != book.group) { + book.copy(group = targetGroup) + } else { + null + } + } + } + if (updateBooks.isNotEmpty()) { + appDb.bookDao.update(*updateBooks.toTypedArray()) + } + }.onError { + context.toastOnUi("更新分组失败\n${it.localizedMessage}") + } + } + fun gotoTop() { scrollTrigger.tryEmit(Unit) }