Jelajahi Sumber

[优化] 优化书架搜索筛选逻辑,可以在书架筛选书源,现在输入关键词时回车触发搜索

HapeLee 6 bulan lalu
induk
melakukan
2c63332403

+ 19 - 3
app/src/main/java/io/legado/app/data/dao/BookDao.kt

@@ -75,6 +75,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -107,6 +108,7 @@ interface BookDao {
         bookUrl,
         bookUrl,
         name,
         name,
         author,
         author,
+        originName,
         coverUrl,
         coverUrl,
         customCoverUrl,
         customCoverUrl,
         durChapterTitle,
         durChapterTitle,
@@ -136,6 +138,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -165,6 +168,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -199,6 +203,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -234,6 +239,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -264,6 +270,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -284,7 +291,9 @@ interface BookDao {
     )
     )
     fun flowBookShelfByUserGroup(group: Long): Flow<List<BookShelfItem>>
     fun flowBookShelfByUserGroup(group: Long): Flow<List<BookShelfItem>>
 
 
-    @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
+    @Query(
+        "SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%' or originName like '%'||:key||'%'"
+    )
     fun flowSearch(key: String): Flow<List<Book>>
     fun flowSearch(key: String): Flow<List<Book>>
 
 
     @Query(
     @Query(
@@ -293,6 +302,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -308,7 +318,7 @@ interface BookDao {
             `order`,
             `order`,
             canUpdate
             canUpdate
         FROM books 
         FROM books 
-        WHERE name like '%'||:key||'%' or author like '%'||:key||'%'
+        WHERE name like '%'||:key||'%' or author like '%'||:key||'%' or originName like '%'||:key||'%'
         """
         """
     )
     )
     fun flowBookShelfSearch(key: String): Flow<List<BookShelfItem>>
     fun flowBookShelfSearch(key: String): Flow<List<BookShelfItem>>
@@ -322,6 +332,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -352,6 +363,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -381,6 +393,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -410,6 +423,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -439,6 +453,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -468,6 +483,7 @@ interface BookDao {
             bookUrl,
             bookUrl,
             name,
             name,
             author,
             author,
+            originName,
             coverUrl,
             coverUrl,
             customCoverUrl,
             customCoverUrl,
             durChapterTitle,
             durChapterTitle,
@@ -582,4 +598,4 @@ interface BookDao {
 
 
     @Query("delete from books where type & ${BookType.notShelf} > 0")
     @Query("delete from books where type & ${BookType.notShelf} > 0")
     fun deleteNotShelfBook()
     fun deleteNotShelfBook()
-}
+}

+ 3 - 2
app/src/main/java/io/legado/app/help/book/BookHelp.kt

@@ -13,6 +13,7 @@ import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.localBook.TextFile
 import io.legado.app.model.localBook.TextFile
@@ -274,7 +275,7 @@ object BookHelp {
         book: Book,
         book: Book,
         bookChapter: BookChapter,
         bookChapter: BookChapter,
         content: String,
         content: String,
-        concurrency: Int = AppConfig.threadCount
+        concurrency: Int = OtherConfig.threadCount
     ) = coroutineScope {
     ) = coroutineScope {
         flowImages(bookChapter, content).onEachParallel(concurrency) { mSrc ->
         flowImages(bookChapter, content).onEachParallel(concurrency) { mSrc ->
             saveImage(bookSource, book, mSrc, bookChapter)
             saveImage(bookSource, book, mSrc, bookChapter)
@@ -673,4 +674,4 @@ object BookHelp {
             .replace(regexOther, "")
             .replace(regexOther, "")
     }
     }
 
 
-}
+}

+ 3 - 2
app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt

@@ -19,6 +19,7 @@ import io.legado.app.model.analyzeRule.AnalyzeRule
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.AnalyzeUrl
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.isTrue
 import io.legado.app.utils.isTrue
 import io.legado.app.utils.mapAsync
 import io.legado.app.utils.mapAsync
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.ensureActive
@@ -96,7 +97,7 @@ object BookChapterList {
                     for (urlStr in chapterData.second) {
                     for (urlStr in chapterData.second) {
                         emit(urlStr)
                         emit(urlStr)
                     }
                     }
-                }.mapAsync(AppConfig.threadCount) { urlStr ->
+                }.mapAsync(OtherConfig.threadCount) { urlStr ->
                     val analyzeUrl = AnalyzeUrl(
                     val analyzeUrl = AnalyzeUrl(
                         mUrl = urlStr,
                         mUrl = urlStr,
                         source = bookSource,
                         source = bookSource,
@@ -285,4 +286,4 @@ object BookChapterList {
         }
         }
     }
     }
 
 
-}
+}

+ 2 - 1
app/src/main/java/io/legado/app/model/webBook/BookContent.kt

@@ -19,6 +19,7 @@ import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setNextChapterUrl
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setNextChapterUrl
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.AnalyzeUrl
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.HtmlFormatter
 import io.legado.app.utils.HtmlFormatter
 import io.legado.app.utils.NetworkUtils
 import io.legado.app.utils.NetworkUtils
 import io.legado.app.utils.mapAsync
 import io.legado.app.utils.mapAsync
@@ -105,7 +106,7 @@ object BookContent {
                 for (urlStr in contentData.second) {
                 for (urlStr in contentData.second) {
                     emit(urlStr)
                     emit(urlStr)
                 }
                 }
-            }.mapAsync(AppConfig.threadCount) { urlStr ->
+            }.mapAsync(OtherConfig.threadCount) { urlStr ->
                 val analyzeUrl = AnalyzeUrl(
                 val analyzeUrl = AnalyzeUrl(
                     mUrl = urlStr,
                     mUrl = urlStr,
                     source = bookSource,
                     source = bookSource,

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/cache/CacheScreen.kt

@@ -241,7 +241,7 @@ private fun CacheScreen(
                         ) { dismiss ->
                         ) { dismiss ->
                             state.groupList.forEach { group ->
                             state.groupList.forEach { group ->
                                 RoundDropdownMenuItem(
                                 RoundDropdownMenuItem(
-                                    text = "${stringResource(R.string.group)}: ${group.groupName}",
+                                    text = "${group.groupName}",
                                     isSelected = group.groupId == state.groupId,
                                     isSelected = group.groupId == state.groupId,
                                     onClick = {
                                     onClick = {
                                         dismiss()
                                         dismiss()

+ 12 - 1
app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt

@@ -394,6 +394,8 @@ fun BookItem(
     titleCenter: Boolean = true,
     titleCenter: Boolean = true,
     titleMaxLines: Int = 2,
     titleMaxLines: Int = 2,
     coverShadow: Boolean = false,
     coverShadow: Boolean = false,
+    isSearchMode: Boolean = false,
+    searchKey: String = "",
     onClick: () -> Unit,
     onClick: () -> Unit,
     onLongClick: () -> Unit
     onLongClick: () -> Unit
 ) {
 ) {
@@ -409,6 +411,15 @@ fun BookItem(
     } else {
     } else {
         null
         null
     }
     }
+    val matchedSourceLabel = if (
+        isSearchMode &&
+        searchKey.isNotBlank() &&
+        book.originName.contains(searchKey, ignoreCase = true)
+    ) {
+        book.originName
+    } else {
+        null
+    }
 
 
     BookshelfItem(
     BookshelfItem(
         isGrid = layoutMode != 0,
         isGrid = layoutMode != 0,
@@ -425,7 +436,7 @@ fun BookItem(
                 modifier = modifier,
                 modifier = modifier,
                 badgeText = if (BookshelfConfig.showUnread && unreadCount > 0) unreadCount.toString() else null,
                 badgeText = if (BookshelfConfig.showUnread && unreadCount > 0) unreadCount.toString() else null,
                 showBadgeDot = BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && book.isNew,
                 showBadgeDot = BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && book.isNew,
-                leftBottomText = bookTypeLabel
+                leftBottomText = matchedSourceLabel ?: bookTypeLabel
             )
             )
         },
         },
         title = book.name,
         title = book.name,

+ 1 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/BookShelfItem.kt

@@ -9,6 +9,7 @@ data class BookShelfItem(
     val bookUrl: String,
     val bookUrl: String,
     val name: String,
     val name: String,
     val author: String,
     val author: String,
+    val originName: String,
     val coverUrl: String?,
     val coverUrl: String?,
     val customCoverUrl: String?,
     val customCoverUrl: String?,
     val durChapterTitle: String?,
     val durChapterTitle: String?,

+ 182 - 52
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -69,9 +69,7 @@ import io.legado.app.base.BaseRuleEvent
 import io.legado.app.data.appDb
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.ui.about.AppLogSheet
 import io.legado.app.ui.about.AppLogSheet
-import io.legado.app.ui.book.cache.CacheActivity
 import io.legado.app.ui.book.info.GroupSelectSheet
 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.config.bookshelfConfig.BookshelfConfig
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeResolver
@@ -82,6 +80,7 @@ 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.SmallOutlinedIconToggleButton
 import io.legado.app.ui.widget.components.button.TopBarActionButton
 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.card.NormalCard
+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.filePicker.FilePickerSheet
 import io.legado.app.ui.widget.components.icon.AppIcons
 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.importComponents.SourceInputDialog
@@ -92,7 +91,6 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.tabRow.AppTabRow
 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.text.AppText
 import io.legado.app.utils.readText
 import io.legado.app.utils.readText
-import io.legado.app.utils.startActivity
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -109,8 +107,10 @@ fun BookshelfScreen(
     viewModel: BookshelfViewModel = koinViewModel(),
     viewModel: BookshelfViewModel = koinViewModel(),
     onBookClick: (BookShelfItem) -> Unit,
     onBookClick: (BookShelfItem) -> Unit,
     onBookLongClick: (BookShelfItem) -> Unit,
     onBookLongClick: (BookShelfItem) -> Unit,
+    onNavigateToSearch: (String) -> Unit,
     onNavigateToRemoteImport: () -> Unit,
     onNavigateToRemoteImport: () -> Unit,
-    onNavigateToLocalImport: () -> Unit
+    onNavigateToLocalImport: () -> Unit,
+    onNavigateToCache: (Long) -> Unit
 ) {
 ) {
     val context = LocalContext.current
     val context = LocalContext.current
     val uiState by viewModel.uiState.collectAsState()
     val uiState by viewModel.uiState.collectAsState()
@@ -182,8 +182,8 @@ fun BookshelfScreen(
         pageCount = { uiState.groups.size }
         pageCount = { uiState.groups.size }
     )
     )
 
 
-    LaunchedEffect(uiState.groups) {
-        if (uiState.groups.isNotEmpty()) {
+    LaunchedEffect(uiState.groups, uiState.isSearch) {
+        if (!uiState.isSearch && uiState.groups.isNotEmpty()) {
             val savedGroupId = BookshelfConfig.saveTabPosition
             val savedGroupId = BookshelfConfig.saveTabPosition
             val savedGroupIndex = uiState.groups.indexOfFirst { it.groupId == savedGroupId }
             val savedGroupIndex = uiState.groups.indexOfFirst { it.groupId == savedGroupId }
             if (savedGroupIndex >= 0 && savedGroupIndex != pagerState.currentPage) {
             if (savedGroupIndex >= 0 && savedGroupIndex != pagerState.currentPage) {
@@ -208,8 +208,16 @@ fun BookshelfScreen(
             }
             }
     }
     }
 
 
-    val currentGroupId =
+    val currentTabGroupId =
         uiState.groups.getOrNull(pagerState.currentPage)?.groupId ?: BookGroup.IdAll
         uiState.groups.getOrNull(pagerState.currentPage)?.groupId ?: BookGroup.IdAll
+    val searchGroupExists = uiState.allGroups.any { it.groupId == uiState.selectedGroupId }
+    val currentGroupId = if (uiState.isSearch && searchGroupExists) {
+        uiState.selectedGroupId
+    } else {
+        currentTabGroupId
+    }
+    val isUsingStandaloneSearchGroup = uiState.isSearch &&
+            uiState.groups.none { it.groupId == currentGroupId }
     val currentGroupBookCountFlow = remember(currentGroupId) {
     val currentGroupBookCountFlow = remember(currentGroupId) {
         appDb.bookDao.flowBookShelfByGroup(currentGroupId).map { it.size }
         appDb.bookDao.flowBookShelfByGroup(currentGroupId).map { it.size }
     }
     }
@@ -266,28 +274,33 @@ fun BookshelfScreen(
         }
         }
     }
     }
 
 
-    val baseTitle = when (bookGroupStyle) {
-        1 -> {
-            uiState.groups.getOrNull(pagerState.currentPage)?.groupName
-                ?: stringResource(R.string.bookshelf)
-        }
-        2 if uiState.groups.isNotEmpty() -> {
+    val currentGroupName = uiState.allGroups.firstOrNull { it.groupId == currentGroupId }?.groupName
+        ?: uiState.groups.getOrNull(pagerState.currentPage)?.groupName
+
+    val baseTitle = when {
+        uiState.isSearch && bookGroupStyle == 0 -> stringResource(R.string.bookshelf)
+        uiState.isSearch -> currentGroupName ?: stringResource(R.string.bookshelf)
+        bookGroupStyle == 1 -> currentGroupName ?: stringResource(R.string.bookshelf)
+        bookGroupStyle == 2 && uiState.groups.isNotEmpty() -> {
             if (isInFolderRoot) stringResource(R.string.bookshelf)
             if (isInFolderRoot) stringResource(R.string.bookshelf)
-            else uiState.groups.getOrNull(pagerState.currentPage)?.groupName
-                ?: stringResource(R.string.bookshelf)
+            else currentGroupName ?: stringResource(R.string.bookshelf)
         }
         }
 
 
         else -> stringResource(R.string.bookshelf)
         else -> stringResource(R.string.bookshelf)
     }
     }
     val title = if (isEditMode) {
     val title = if (isEditMode) {
-        "$baseTitle • ${selectedBookUrls.size} / $currentGroupBookCount"
+        "$currentGroupBookCount · ${selectedBookUrls.size} 本"
     } else if (uiState.upBooksCount > 0) {
     } else if (uiState.upBooksCount > 0) {
         "$baseTitle (${uiState.upBooksCount})"
         "$baseTitle (${uiState.upBooksCount})"
     } else {
     } else {
         baseTitle
         baseTitle
     }
     }
     val subtitle = if (isEditMode) {
     val subtitle = if (isEditMode) {
-        "${stringResource(R.string.all)} • ${stringResource(R.string.book_count, allGroupsBookCount)}"
+        if (bookGroupStyle == 0) {
+            "共${allGroupsBookCount}本"
+        } else {
+            "${currentGroupName ?: stringResource(R.string.bookshelf)} · 共${allGroupsBookCount}本"
+        }
     } else {
     } else {
         null
         null
     }
     }
@@ -317,9 +330,28 @@ fun BookshelfScreen(
         state = uiState,
         state = uiState,
         onBackClick = if (isEditMode) exitEditMode else null,
         onBackClick = if (isEditMode) exitEditMode else null,
         backNavigationIcon = if (isEditMode) AppIcons.Close else AppIcons.Back,
         backNavigationIcon = if (isEditMode) AppIcons.Close else AppIcons.Back,
-        showSearchAction = !isEditMode,
-        onSearchToggle = { context.startActivity<SearchActivity>() },
+        showSearchAction = true,
+        onSearchToggle = { active ->
+            viewModel.setSearchMode(active)
+            if (!active && uiState.selectedGroupId != currentTabGroupId) {
+                viewModel.changeGroup(currentTabGroupId)
+            }
+        },
         onSearchQueryChange = { viewModel.setSearchKey(it) },
         onSearchQueryChange = { viewModel.setSearchKey(it) },
+        onSearchSubmit = { rawQuery ->
+            rawQuery.trim()
+                .takeIf { it.isNotEmpty() }
+                ?.let(onNavigateToSearch)
+        },
+        searchTrailingIcon = {
+            if (uiState.searchKey.isNotEmpty()) {
+                TopBarActionButton(
+                    onClick = { viewModel.setSearchKey("") },
+                    imageVector = AppIcons.Close,
+                    contentDescription = stringResource(R.string.clear)
+                )
+            }
+        },
         topBarActions = {
         topBarActions = {
             if (isEditMode) {
             if (isEditMode) {
                 TopBarActionButton(
                 TopBarActionButton(
@@ -393,9 +425,7 @@ fun BookshelfScreen(
                     onClick = {
                     onClick = {
                         val groupId =
                         val groupId =
                             uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
                             uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
-                        context.startActivity<CacheActivity> {
-                            putExtra("groupId", groupId)
-                        }
+                        onNavigateToCache(groupId)
                         dismiss()
                         dismiss()
                     },
                     },
                     leadingIcon = { Icon(Icons.Default.Download, null) }
                     leadingIcon = { Icon(Icons.Default.Download, null) }
@@ -463,11 +493,19 @@ fun BookshelfScreen(
                                         RoundDropdownMenuItem(
                                         RoundDropdownMenuItem(
                                             text = group.groupName,
                                             text = group.groupName,
                                             onClick = {
                                             onClick = {
+                                                if (uiState.isSearch) {
+                                                    viewModel.changeGroup(group.groupId)
+                                                }
                                                 scope.launch { pagerState.animateScrollToPage(index) }
                                                 scope.launch { pagerState.animateScrollToPage(index) }
                                                 dismiss()
                                                 dismiss()
                                             },
                                             },
                                             trailingIcon = {
                                             trailingIcon = {
-                                                if (selectedTabIndex == index) {
+                                                val isSelected = if (uiState.isSearch) {
+                                                    uiState.selectedGroupId == group.groupId
+                                                } else {
+                                                    selectedTabIndex == index
+                                                }
+                                                if (isSelected) {
                                                     Icon(
                                                     Icon(
                                                         Icons.Default.Check,
                                                         Icons.Default.Check,
                                                         null,
                                                         null,
@@ -477,6 +515,59 @@ fun BookshelfScreen(
                                             }
                                             }
                                         )
                                         )
                                     }
                                     }
+
+                                    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)
+                                                            )
+                                                        }
+                                                    }
+                                                )
+                                            }
+                                        }
+                                    }
                                 }
                                 }
                             }
                             }
                         }
                         }
@@ -487,7 +578,11 @@ fun BookshelfScreen(
     ) { paddingValues ->
     ) { paddingValues ->
         var isRefreshing by remember { mutableStateOf(false) }
         var isRefreshing by remember { mutableStateOf(false) }
         val pullToRefreshState = rememberPullToRefreshState()
         val pullToRefreshState = rememberPullToRefreshState()
-        val currentGroup = uiState.groups.getOrNull(pagerState.currentPage)
+        val currentGroup = if (uiState.isSearch) {
+            uiState.allGroups.firstOrNull { it.groupId == currentGroupId }
+        } else {
+            uiState.groups.getOrNull(pagerState.currentPage)
+        }
         val pullToRefreshEnabled = (currentGroup?.enableRefresh ?: true) && !isEditMode
         val pullToRefreshEnabled = (currentGroup?.enableRefresh ?: true) && !isEditMode
 
 
         Box(
         Box(
@@ -511,7 +606,7 @@ fun BookshelfScreen(
                 targetState = isInFolderRoot,
                 targetState = isInFolderRoot,
                 label = "FolderTransition"
                 label = "FolderTransition"
             ) { isRoot ->
             ) { isRoot ->
-                if (bookGroupStyle == 2 && isRoot) {
+                if (bookGroupStyle == 2 && isRoot && !isUsingStandaloneSearchGroup) {
                     val folderColumns =
                     val folderColumns =
                         if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
                         if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
                     val isGridMode = bookshelfLayoutMode != 0
                     val isGridMode = bookshelfLayoutMode != 0
@@ -576,32 +671,50 @@ fun BookshelfScreen(
                         }
                         }
                     }
                     }
                 } else {
                 } else {
-                    HorizontalPager(
-                        state = pagerState,
-                        modifier = Modifier.fillMaxSize(),
-                        beyondViewportPageCount = 3,
-                        key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
-                    ) { pageIndex ->
-                        val group = uiState.groups.getOrNull(pageIndex)
-                        if (group != null) {
-                            val booksFlow = remember(group.groupId) {
-                                viewModel.getBooksFlow(group.groupId)
-                            }
-                            val books by booksFlow.collectAsState(emptyList())
-                            BookshelfPage(
-                                paddingValues = paddingValues,
-                                books = books,
-                                uiState = uiState,
-                                bookshelfLayoutMode = bookshelfLayoutMode,
-                                bookshelfLayoutGrid = bookshelfLayoutGrid,
+                    if (isUsingStandaloneSearchGroup) {
+                        BookshelfPage(
+                            paddingValues = paddingValues,
+                            books = uiState.items,
+                            uiState = uiState,
+                            bookshelfLayoutMode = bookshelfLayoutMode,
+                            bookshelfLayoutGrid = bookshelfLayoutGrid,
+                            bookshelfLayoutList = bookshelfLayoutList,
+                            isEditMode = isEditMode,
+                            selectedBookUrls = selectedBookUrls,
+                            onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
+                            onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
+                            onBookClick = onBookClick,
+                            onBookLongClick = onBookLongClick
+                        )
+                    } else {
+                        HorizontalPager(
+                            state = pagerState,
+                            modifier = Modifier.fillMaxSize(),
+                            beyondViewportPageCount = 3,
+                            key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
+                        ) { pageIndex ->
+                            val group = uiState.groups.getOrNull(pageIndex)
+                            if (group != null) {
+                                val booksFlow = remember(group.groupId) {
+                                    viewModel.getBooksFlow(group.groupId)
+                                }
+                                val books by booksFlow.collectAsState(emptyList())
+                                BookshelfPage(
+                                    paddingValues = paddingValues,
+                                    books = books,
+                                    uiState = uiState,
+                                    bookshelfLayoutMode = bookshelfLayoutMode,
+                                    bookshelfLayoutGrid = bookshelfLayoutGrid,
                                 bookshelfLayoutList = bookshelfLayoutList,
                                 bookshelfLayoutList = bookshelfLayoutList,
                                 isEditMode = isEditMode,
                                 isEditMode = isEditMode,
                                 selectedBookUrls = selectedBookUrls,
                                 selectedBookUrls = selectedBookUrls,
                                 onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
                                 onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
+                                onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
                                 onBookClick = onBookClick,
                                 onBookClick = onBookClick,
                                 onBookLongClick = onBookLongClick
                                 onBookLongClick = onBookLongClick
                             )
                             )
                         }
                         }
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -716,19 +829,34 @@ fun BookshelfPage(
     isEditMode: Boolean,
     isEditMode: Boolean,
     selectedBookUrls: Set<String>,
     selectedBookUrls: Set<String>,
     onToggleBookSelection: (BookShelfItem) -> Unit,
     onToggleBookSelection: (BookShelfItem) -> Unit,
+    onGlobalSearch: () -> Unit,
     onBookClick: (BookShelfItem) -> Unit,
     onBookClick: (BookShelfItem) -> Unit,
     onBookLongClick: (BookShelfItem) -> Unit
     onBookLongClick: (BookShelfItem) -> Unit
 ) {
 ) {
     if (books.isEmpty()) {
     if (books.isEmpty()) {
-        EmptyMessage(
-            modifier = Modifier
-                .fillMaxSize()
-                .padding(
-                    top = paddingValues.calculateTopPadding(),
-                    bottom = paddingValues.calculateBottomPadding()
-                ),
-            messageResId = R.string.bookshelf_empty
-        )
+        if (uiState.isSearch) {
+            EmptyMessage(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(
+                        top = paddingValues.calculateTopPadding(),
+                        bottom = paddingValues.calculateBottomPadding()
+                    ),
+                message = "没有书籍,尝试全局搜索",
+                buttonText = "全局搜索",
+                onButtonClick = onGlobalSearch
+            )
+        } else {
+            EmptyMessage(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(
+                        top = paddingValues.calculateTopPadding(),
+                        bottom = paddingValues.calculateBottomPadding()
+                    ),
+                messageResId = R.string.bookshelf_empty
+            )
+        }
         return
         return
     }
     }
 
 
@@ -766,6 +894,8 @@ fun BookshelfPage(
                 titleCenter = BookshelfConfig.bookshelfTitleCenter,
                 titleCenter = BookshelfConfig.bookshelfTitleCenter,
                 titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
                 titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
                 coverShadow = BookshelfConfig.bookshelfCoverShadow,
                 coverShadow = BookshelfConfig.bookshelfCoverShadow,
+                isSearchMode = uiState.isSearch,
+                searchKey = uiState.searchKey,
                 onClick = {
                 onClick = {
                     if (isEditMode) {
                     if (isEditMode) {
                         onToggleBookSelection(book)
                         onToggleBookSelection(book)

+ 2 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt

@@ -10,9 +10,11 @@ data class BookshelfUiState(
     override val isSearch: Boolean = false,
     override val isSearch: Boolean = false,
     override val isLoading: Boolean = false,
     override val isLoading: Boolean = false,
     val groups: List<BookGroup> = emptyList(),
     val groups: List<BookGroup> = emptyList(),
+    val allGroups: List<BookGroup> = emptyList(),
     val groupPreviews: Map<Long, List<BookShelfItem>> = emptyMap(),
     val groupPreviews: Map<Long, List<BookShelfItem>> = emptyMap(),
     val groupBookCounts: Map<Long, Int> = emptyMap(),
     val groupBookCounts: Map<Long, Int> = emptyMap(),
     val selectedGroupIndex: Int = 0,
     val selectedGroupIndex: Int = 0,
+    val selectedGroupId: Long = BookGroup.IdAll,
     val loadingText: String? = null,
     val loadingText: String? = null,
     val upBooksCount: Int = 0,
     val upBooksCount: Int = 0,
     val updatingBooks: Set<String> = emptySet()
     val updatingBooks: Set<String> = emptySet()

+ 37 - 18
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -97,6 +97,7 @@ class BookshelfViewModel(
 
 
     private val groupIdFlow = MutableStateFlow(BookshelfConfig.saveTabPosition)
     private val groupIdFlow = MutableStateFlow(BookshelfConfig.saveTabPosition)
     private val searchKeyFlow = MutableStateFlow("")
     private val searchKeyFlow = MutableStateFlow("")
+    private val searchModeFlow = MutableStateFlow(false)
     private val refreshTrigger = MutableStateFlow(0)
     private val refreshTrigger = MutableStateFlow(0)
     private val loadingTextFlow = MutableStateFlow<String?>(null)
     private val loadingTextFlow = MutableStateFlow<String?>(null)
 
 
@@ -207,19 +208,27 @@ class BookshelfViewModel(
             }
             }
         }.distinctUntilChanged().flowOn(Dispatchers.Default)
         }.distinctUntilChanged().flowOn(Dispatchers.Default)
 
 
-    private val internalStateFlow = combine(
+    private val coreInternalStateFlow = combine(
         groupIdFlow,
         groupIdFlow,
         searchKeyFlow,
         searchKeyFlow,
+        searchModeFlow,
         loadingTextFlow,
         loadingTextFlow,
-        updatingBooksFlow,
+        updatingBooksFlow
+    ) { groupId, searchKey, isSearchMode, loadingText, updatingBooks ->
+        InternalState(groupId, searchKey, isSearchMode, loadingText, updatingBooks, 0)
+    }
+
+    private val internalStateFlow = combine(
+        coreInternalStateFlow,
         upBooksCountFlow
         upBooksCountFlow
-    ) { groupId, searchKey, loadingText, updatingBooks, upBooksCount ->
-        InternalState(groupId, searchKey, loadingText, updatingBooks, upBooksCount)
+    ) { core, upBooksCount ->
+        core.copy(upBooksCount = upBooksCount)
     }
     }
 
 
     data class InternalState(
     data class InternalState(
         val groupId: Long,
         val groupId: Long,
         val searchKey: String,
         val searchKey: String,
+        val isSearchMode: Boolean,
         val loadingText: String?,
         val loadingText: String?,
         val updatingBooks: Set<String>,
         val updatingBooks: Set<String>,
         val upBooksCount: Int
         val upBooksCount: Int
@@ -228,29 +237,27 @@ class BookshelfViewModel(
     val uiState: StateFlow<BookshelfUiState> = combine(
     val uiState: StateFlow<BookshelfUiState> = combine(
         booksFlow,
         booksFlow,
         groupsFlow,
         groupsFlow,
+        allGroupsFlow,
         groupPreviewsFlow,
         groupPreviewsFlow,
         internalStateFlow
         internalStateFlow
-    ) { books, groups, previews, internal ->
-        val filteredBooks = if (internal.searchKey.isEmpty()) {
+    ) { books, groups, allGroups, previews, internal ->
+        val filteredBooks = if (!internal.isSearchMode || internal.searchKey.isBlank()) {
             books
             books
         } else {
         } else {
-            books.filter {
-                it.name.contains(
-                    internal.searchKey,
-                    true
-                ) || it.author.contains(internal.searchKey, true)
-            }
+            books.filter { it.matchesSearchKey(internal.searchKey) }
         }
         }
 
 
         BookshelfUiState(
         BookshelfUiState(
             items = filteredBooks,
             items = filteredBooks,
             groups = groups,
             groups = groups,
+            allGroups = allGroups,
             groupPreviews = previews.previews,
             groupPreviews = previews.previews,
             groupBookCounts = previews.counts,
             groupBookCounts = previews.counts,
             selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId }
             selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId }
                 .coerceAtLeast(0),
                 .coerceAtLeast(0),
+            selectedGroupId = internal.groupId,
             searchKey = internal.searchKey,
             searchKey = internal.searchKey,
-            isSearch = internal.searchKey.isNotEmpty(),
+            isSearch = internal.isSearchMode,
             isLoading = internal.loadingText != null,
             isLoading = internal.loadingText != null,
             loadingText = internal.loadingText,
             loadingText = internal.loadingText,
             upBooksCount = internal.upBooksCount,
             upBooksCount = internal.upBooksCount,
@@ -333,16 +340,15 @@ class BookshelfViewModel(
         return combine(
         return combine(
             appDb.bookDao.flowBookShelfByGroup(groupId),
             appDb.bookDao.flowBookShelfByGroup(groupId),
             searchKeyFlow,
             searchKeyFlow,
+            searchModeFlow,
             groupsFlow,
             groupsFlow,
             refreshTrigger
             refreshTrigger
-        ) { books, searchKey, groups, _ ->
+        ) { books, searchKey, isSearchMode, groups, _ ->
             val group = groups.find { it.groupId == groupId }
             val group = groups.find { it.groupId == groupId }
-            val filtered = if (searchKey.isEmpty()) {
+            val filtered = if (!isSearchMode || searchKey.isBlank()) {
                 books
                 books
             } else {
             } else {
-                books.filter {
-                    it.name.contains(searchKey, true) || it.author.contains(searchKey, true)
-                }
+                books.filter { it.matchesSearchKey(searchKey) }
             }
             }
             sortBooks(filtered, group)
             sortBooks(filtered, group)
         }.distinctUntilChanged().flowOn(Dispatchers.Default)
         }.distinctUntilChanged().flowOn(Dispatchers.Default)
@@ -359,6 +365,13 @@ class BookshelfViewModel(
         searchKeyFlow.value = key
         searchKeyFlow.value = key
     }
     }
 
 
+    fun setSearchMode(active: Boolean) {
+        searchModeFlow.value = active
+        if (!active) {
+            searchKeyFlow.value = ""
+        }
+    }
+
     fun refresh() {
     fun refresh() {
         refreshTrigger.value++
         refreshTrigger.value++
     }
     }
@@ -764,4 +777,10 @@ class BookshelfViewModel(
         }
         }
     }
     }
 
 
+    private fun BookShelfItem.matchesSearchKey(searchKey: String): Boolean {
+        return name.contains(searchKey, true) ||
+                author.contains(searchKey, true) ||
+                originName.contains(searchKey, true)
+    }
+
 }
 }

+ 22 - 1
app/src/main/java/io/legado/app/ui/widget/components/EmptyMessage.kt

@@ -18,12 +18,15 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.sp
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.icon.AppIcons
+import io.legado.app.ui.widget.components.button.SmallTonalTextButton
 import io.legado.app.ui.widget.components.text.AnimatedTextLine
 import io.legado.app.ui.widget.components.text.AnimatedTextLine
 
 
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -32,6 +35,9 @@ fun EmptyMessage(
     message: String,
     message: String,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
     isLoading: Boolean = false,
     isLoading: Boolean = false,
+    buttonText: String? = null,
+    buttonImageVector: ImageVector = AppIcons.Search,
+    onButtonClick: (() -> Unit)? = null,
     faces: List<String> = listOf(
     faces: List<String> = listOf(
         "(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)",
         "(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)",
         "(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)"
         "(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)"
@@ -77,6 +83,15 @@ fun EmptyMessage(
             softWrap = true,
             softWrap = true,
             modifier = Modifier.widthIn(max = 240.dp)
             modifier = Modifier.widthIn(max = 240.dp)
         )
         )
+
+        if (buttonText != null && onButtonClick != null) {
+            Spacer(modifier = Modifier.height(8.dp))
+            SmallTonalTextButton(
+                onClick = onButtonClick,
+                text = buttonText,
+                imageVector = buttonImageVector
+            )
+        }
     }
     }
 }
 }
 
 
@@ -85,6 +100,9 @@ fun EmptyMessage(
     @StringRes messageResId: Int,
     @StringRes messageResId: Int,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
     isLoading: Boolean = false,
     isLoading: Boolean = false,
+    buttonText: String? = null,
+    buttonImageVector: ImageVector = AppIcons.Search,
+    onButtonClick: (() -> Unit)? = null,
     faces: List<String> = listOf(
     faces: List<String> = listOf(
         "(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)",
         "(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)",
         "(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)"
         "(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)"
@@ -97,8 +115,11 @@ fun EmptyMessage(
         message = message,
         message = message,
         modifier = modifier,
         modifier = modifier,
         isLoading = isLoading,
         isLoading = isLoading,
+        buttonText = buttonText,
+        buttonImageVector = buttonImageVector,
+        onButtonClick = onButtonClick,
         faces = faces,
         faces = faces,
         faceTextSize = faceTextSize,
         faceTextSize = faceTextSize,
         onFaceClick = onFaceClick
         onFaceClick = onFaceClick
     )
     )
-}
+}

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt

@@ -169,7 +169,7 @@ fun RoundDropdownMenuItem(
                     Text(
                     Text(
                         modifier = Modifier.widthIn(max = 200.dp),
                         modifier = Modifier.widthIn(max = 200.dp),
                         text = text,
                         text = text,
-                        style = LegadoTheme.typography.bodyMediumEmphasized,
+                        style = LegadoTheme.typography.labelLargeEmphasized,
                         color = contentColor
                         color = contentColor
                     )
                     )
                 }
                 }