Explorar el Código

fix: 搜索界面问题

HapeLee hace 4 meses
padre
commit
fc096c92e8

+ 2 - 1
app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt

@@ -509,7 +509,8 @@ fun SearchScreen(
                                                 },
                                                 },
                                                 sharedTransitionScope = sharedTransitionScope,
                                                 sharedTransitionScope = sharedTransitionScope,
                                                 animatedVisibilityScope = animatedVisibilityScope,
                                                 animatedVisibilityScope = animatedVisibilityScope,
-                                                sharedCoverKey = sharedCoverKey
+                                                sharedCoverKey = sharedCoverKey,
+                                                sourceCount = item.book.origins.size,
                                             )
                                             )
                                         }
                                         }
 
 

+ 69 - 7
app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt

@@ -84,10 +84,11 @@ class SearchViewModel(
     private val bookshelfKeys = MutableStateFlow<Set<BookShelfKey>>(emptySet())
     private val bookshelfKeys = MutableStateFlow<Set<BookShelfKey>>(emptySet())
     private val searchScope = SearchScope(AppConfig.searchScope)
     private val searchScope = SearchScope(AppConfig.searchScope)
     private val searchControl = BookSearchControl()
     private val searchControl = BookSearchControl()
-    private val searchResultBooks = LinkedHashMap<String, SearchBook>()
+    private val searchResultBooks = LinkedHashMap<SearchResultKey, SearchBook>()
 
 
     private var searchJob: Job? = null
     private var searchJob: Job? = null
     private var currentSearchPage = 1
     private var currentSearchPage = 1
+    private var resultCountBeforeCurrentPage = 0
     private var wasSearching = false
     private var wasSearching = false
 
 
     init {
     init {
@@ -483,6 +484,7 @@ class SearchViewModel(
     private fun startSearch(keyword: String, page: Int) {
     private fun startSearch(keyword: String, page: Int) {
         searchJob?.cancel()
         searchJob?.cancel()
         searchControl.resume()
         searchControl.resume()
+        resultCountBeforeCurrentPage = searchResultBooks.size
         wasSearching = true
         wasSearching = true
         searchJob = viewModelScope.launch {
         searchJob = viewModelScope.launch {
             try {
             try {
@@ -517,10 +519,8 @@ class SearchViewModel(
             }
             }
 
 
             is SearchRunEvent.Progress -> {
             is SearchRunEvent.Progress -> {
-                event.removedBookUrls.forEach { searchResultBooks.remove(it) }
-                event.upsertBooks.forEach { book ->
-                    searchResultBooks[book.bookUrl] = book
-                }
+                removeSearchResults(event.removedBookUrls)
+                mergeSearchResults(event.upsertBooks)
                 _uiState.update {
                 _uiState.update {
                     it.copy(
                     it.copy(
                         results = buildSearchResultItems(
                         results = buildSearchResultItems(
@@ -542,9 +542,16 @@ class SearchViewModel(
                     } else {
                     } else {
                         null
                         null
                     }
                     }
+                    val hasNewPageResults = searchResultBooks.size > resultCountBeforeCurrentPage
+                    val exactSearchShouldStopAfterFirstPage =
+                        state.matchMode == MatchMode.EXACT &&
+                            currentSearchPage == 1 &&
+                            searchResultBooks.size <= EXACT_SEARCH_SINGLE_PAGE_RESULT_THRESHOLD
                     state.copy(
                     state.copy(
                         isSearching = false,
                         isSearching = false,
-                        hasMore = event.hasMore,
+                        hasMore = event.hasMore &&
+                            hasNewPageResults &&
+                            !exactSearchShouldStopAfterFirstPage,
                         emptyScopeAction = emptyAction,
                         emptyScopeAction = emptyAction,
                     )
                     )
                 }
                 }
@@ -697,7 +704,9 @@ class SearchViewModel(
     private fun buildSearchResultItems(
     private fun buildSearchResultItems(
         shelf: Set<BookShelfKey>,
         shelf: Set<BookShelfKey>,
     ): List<SearchResultItemUi> {
     ): List<SearchResultItemUi> {
-        return searchResultBooks.values.toList().toSearchResultItems(shelf)
+        return searchResultBooks.values
+            .sortedWithSearchPriority(_uiState.value.committedQuery, _uiState.value.matchMode)
+            .toSearchResultItems(shelf)
     }
     }
 
 
     private fun List<SearchResultItemUi>.withShelfState(
     private fun List<SearchResultItemUi>.withShelfState(
@@ -749,4 +758,57 @@ class SearchViewModel(
     private fun emitEffect(effect: SearchEffect) {
     private fun emitEffect(effect: SearchEffect) {
         _effects.tryEmit(effect)
         _effects.tryEmit(effect)
     }
     }
+
+    private fun mergeSearchResults(books: List<SearchBook>) {
+        books.forEach { book ->
+            val key = SearchResultKey(book.name, book.author)
+            val currentBook = searchResultBooks[key]
+            if (currentBook == null) {
+                searchResultBooks[key] = book
+            } else {
+                book.origins.forEach { origin -> currentBook.addOrigin(origin) }
+            }
+        }
+    }
+
+    private fun removeSearchResults(bookUrls: List<String>) {
+        if (bookUrls.isEmpty()) return
+        val removedUrls = bookUrls.toHashSet()
+        searchResultBooks.entries.removeAll { (_, book) -> book.bookUrl in removedUrls }
+    }
+
+    private fun Collection<SearchBook>.sortedWithSearchPriority(
+        keyword: String,
+        matchMode: MatchMode,
+    ): List<SearchBook> {
+        val equalBooks = arrayListOf<SearchBook>()
+        val tagsBooks = arrayListOf<SearchBook>()
+        val containsBooks = arrayListOf<SearchBook>()
+        val otherBooks = arrayListOf<SearchBook>()
+        forEach { book ->
+            when {
+                book.name.equals(keyword, ignoreCase = true) ||
+                    book.author.equals(keyword, ignoreCase = true) -> equalBooks.add(book)
+                book.kind?.contains(keyword, ignoreCase = true) == true -> tagsBooks.add(book)
+                book.name.contains(keyword, ignoreCase = true) ||
+                    book.author.contains(keyword, ignoreCase = true) -> containsBooks.add(book)
+                matchMode == MatchMode.DEFAULT -> otherBooks.add(book)
+            }
+        }
+        return buildList(size) {
+            addAll(equalBooks.sortedByDescending { it.origins.size })
+            addAll(tagsBooks.sortedByDescending { it.origins.size })
+            addAll(containsBooks.sortedByDescending { it.origins.size })
+            addAll(otherBooks)
+        }
+    }
+
+    private data class SearchResultKey(
+        val name: String,
+        val author: String,
+    )
+
+    private companion object {
+        const val EXACT_SEARCH_SINGLE_PAGE_RESULT_THRESHOLD = 3
+    }
 }
 }

+ 22 - 6
app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt

@@ -28,9 +28,11 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
+import io.legado.app.R
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.domain.model.BookShelfState
 import io.legado.app.domain.model.BookShelfState
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
@@ -53,6 +55,7 @@ fun SearchBookListItem(
     sharedTransitionScope: SharedTransitionScope? = null,
     sharedTransitionScope: SharedTransitionScope? = null,
     animatedVisibilityScope: AnimatedVisibilityScope? = null,
     animatedVisibilityScope: AnimatedVisibilityScope? = null,
     sharedCoverKey: String? = null,
     sharedCoverKey: String? = null,
+    sourceCount: Int? = null,
 ) {
 ) {
     Row(
     Row(
         modifier = modifier
         modifier = modifier
@@ -106,12 +109,25 @@ fun SearchBookListItem(
                 .weight(1f)
                 .weight(1f)
                 .align(Alignment.CenterVertically)
                 .align(Alignment.CenterVertically)
         ) {
         ) {
-            AppText(
-                text = book.name,
-                style = LegadoTheme.typography.titleSmall,
-                maxLines = 1,
-                overflow = TextOverflow.Ellipsis,
-            )
+            Row(verticalAlignment = Alignment.CenterVertically) {
+                AppText(
+                    text = book.name,
+                    modifier = Modifier.weight(1f),
+                    style = LegadoTheme.typography.titleSmall,
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis,
+                )
+
+                sourceCount?.let { count ->
+                    Spacer(modifier = Modifier.width(8.dp))
+                    TextCard(
+                        text = stringResource(R.string.search_book_source_count, count),
+                        cornerRadius = 4.dp,
+                        horizontalPadding = 4.dp,
+                        verticalPadding = 2.dp
+                    )
+                }
+            }
 
 
             Row {
             Row {
                 AppText(
                 AppText(

+ 1 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -1724,6 +1724,7 @@
     <string name="search_empty_scope_switch_all">%1$s分组搜索结果为空,是否切换到全部分组?</string>
     <string name="search_empty_scope_switch_all">%1$s分组搜索结果为空,是否切换到全部分组?</string>
     <string name="search_type">搜索类型</string>
     <string name="search_type">搜索类型</string>
     <string name="search_has_more">上拉或点击按钮加载更多</string>
     <string name="search_has_more">上拉或点击按钮加载更多</string>
+    <string name="search_book_source_count">%1$d</string>
     <string name="search_layout_list">列表布局</string>
     <string name="search_layout_list">列表布局</string>
     <string name="search_layout_source_grouped">来源分组</string>
     <string name="search_layout_source_grouped">来源分组</string>
     <string name="download_cache_config">下载与缓存</string>
     <string name="download_cache_config">下载与缓存</string>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -1760,6 +1760,7 @@
     <string name="search_empty_scope_switch_all">%1$s group search returned no results. Switch to all groups?</string>
     <string name="search_empty_scope_switch_all">%1$s group search returned no results. Switch to all groups?</string>
     <string name="search_type">Search type</string>
     <string name="search_type">Search type</string>
     <string name="search_has_more">Pull up or tap button to load more</string>
     <string name="search_has_more">Pull up or tap button to load more</string>
+    <string name="search_book_source_count">%1$d</string>
     <string name="search_layout_list">List layout</string>
     <string name="search_layout_list">List layout</string>
     <string name="search_layout_source_grouped">Source grouped</string>
     <string name="search_layout_source_grouped">Source grouped</string>
     <string name="download_cache_config">Download &amp; Cache</string>
     <string name="download_cache_config">Download &amp; Cache</string>