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

[优化] 新增书架界面的书籍管理

HapeLee 6 месяцев назад
Родитель
Сommit
5455600eba

+ 11 - 0
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(

+ 191 - 83
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<Set<String>>(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<SearchActivity>() },
         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<BookshelfManageActivity> {
-                        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<CacheActivity> {
-                        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<CacheActivity> {
+                            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<String>,
+    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)
+                    }
+                }
             )
         }
     }
 }
-

+ 28 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -363,6 +363,34 @@ class BookshelfViewModel(
         refreshTrigger.value++
     }
 
+    fun moveBooksToGroup(bookUrls: Set<String>, groupId: Long) {
+        updateBooksGroup(bookUrls) { groupId }
+    }
+
+    private fun updateBooksGroup(
+        bookUrls: Set<String>,
+        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)
     }