فهرست منبع

修复几个遗留问题

Squash merge PR #1870
5151561 3 ماه پیش
والد
کامیت
7d032a13d8

+ 2 - 1
app/src/main/java/io/legado/app/di/appModule.kt

@@ -644,7 +644,8 @@ val appModule = module {
             initialSearchWord = route.searchWord,
             searchResultIndex = route.searchResultIndex,
             bookRepository = get(),
-            searchContentRepository = get()
+            searchContentRepository = get(),
+            themeSettingsGateway = get(),
         )
     }
 }

+ 2 - 8
app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt

@@ -624,16 +624,10 @@ class PageView(
             if (!it.estimated && it.allPreviousChaptersExact) {
                 context.getString(R.string.whole_book_page_format, it.currentPage, it.totalPages)
             } else {
-                val currentPage = if (it.allPreviousChaptersExact) {
-                    it.currentPage.toString()
-                } else {
-                    "≈${it.currentPage}"
-                }
-                val totalPages = if (it.estimated) "≈${it.totalPages}" else it.totalPages.toString()
                 context.getString(
                     R.string.whole_book_page_estimated_format,
-                    currentPage,
-                    totalPages,
+                    it.currentPage.toString(),
+                    it.totalPages.toString(),
                 )
             }
         }

+ 8 - 2
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt

@@ -6,6 +6,7 @@ import androidx.core.view.isVisible
 import io.legado.app.base.adapter.ItemViewHolder
 import io.legado.app.base.adapter.RecyclerAdapter
 import io.legado.app.databinding.ItemSearchListBinding
+import io.legado.app.help.config.AppConfig
 import io.legado.app.utils.themeColor
 
 
@@ -29,8 +30,13 @@ class SearchContentAdapter(context: Context, val callback: Callback) :
         binding.run {
             val isDur = callback.durChapterIndex() == item.chapterIndex
             if (payloads.isEmpty()) {
-                tvTitle.text = item.getTitleSpannable(textColorInt)
-                tvContent.text = item.getContentSpannable(textColorInt, accentColorInt, bgColorInt)
+                tvTitle.text = item.getTitleSpannable(textColorInt, AppConfig.isEInkMode)
+                tvContent.text = item.getContentSpannable(
+                    textColorInt,
+                    accentColorInt,
+                    bgColorInt,
+                    AppConfig.isEInkMode,
+                )
 
                 if (item.progressPercent > 0) {
                     cdCount.isVisible = true

+ 22 - 18
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt

@@ -44,10 +44,10 @@ import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -119,11 +119,18 @@ fun SearchContentScreen(
     val isSearching = state.isSearching
     val searchResults = state.searchResults
     val durChapterIndex = state.durChapterIndex
+    val isEInkMode = state.isEInkMode
     val error = state.error
 
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     val listState = rememberLazyListState()
     val scope = rememberCoroutineScope()
+    val focusManager = LocalFocusManager.current
+    val keyboardController = LocalSoftwareKeyboardController.current
+    val hideSearchKeyboard = {
+        focusManager.clearFocus(force = true)
+        keyboardController?.hide()
+    }
 
     val scrollToCurrentChapter = {
         val targetIndex = searchResults.indexOfFirst { it.chapterIndex == durChapterIndex }
@@ -276,6 +283,7 @@ fun SearchContentScreen(
                             history = searchHistory,
                             onlyThisBook = historyOnlyThisBook,
                             onHistoryClick = {
+                                hideSearchKeyboard()
                                 onIntent(SearchContentIntent.UpdateQuery(it.query))
                             },
                             onDeleteHistory = {
@@ -303,6 +311,7 @@ fun SearchContentScreen(
                                 SearchResultItem(
                                     modifier = Modifier.animateItem(),
                                     result = result,
+                                    isEInkMode = isEInkMode,
                                     isCurrentChapter = result.chapterIndex == durChapterIndex,
                                     onClick = {
                                         onIntent(SearchContentIntent.OpenResult(result))
@@ -415,6 +424,7 @@ fun SearchHistoryList(
 fun SearchResultItem(
     modifier: Modifier,
     result: SearchResult,
+    isEInkMode: Boolean,
     isCurrentChapter: Boolean,
     onClick: () -> Unit
 ) {
@@ -432,13 +442,10 @@ fun SearchResultItem(
 
             Column {
                 AppText(
-                    text = buildAnnotatedString {
-                        append(
-                            result.getTitleSpannable(
-                                LegadoTheme.colorScheme.primary.toArgb()
-                            )
-                        )
-                    },
+                    text = result.getTitleAnnotatedString(
+                        accentColor = LegadoTheme.colorScheme.primary,
+                        isEInkMode = isEInkMode,
+                    ),
                     style = LegadoTheme.typography.titleSmall
                 )
 
@@ -449,15 +456,12 @@ fun SearchResultItem(
                 Spacer(modifier = Modifier.height(8.dp))
 
                 AppText(
-                    text = buildAnnotatedString {
-                        append(
-                            result.getContentSpannable(
-                                textColor = LegadoTheme.colorScheme.onSurface.toArgb(),
-                                accentColor = LegadoTheme.colorScheme.primary.toArgb(),
-                                bgColor = LegadoTheme.colorScheme.primaryContainer.toArgb()
-                            )
-                        )
-                    },
+                    text = result.getContentAnnotatedString(
+                        textColor = LegadoTheme.colorScheme.onSurface,
+                        accentColor = LegadoTheme.colorScheme.primary,
+                        backgroundColor = LegadoTheme.colorScheme.primaryContainer,
+                        isEInkMode = isEInkMode,
+                    ),
                     style = LegadoTheme.typography.bodyMedium
                 )
             }

+ 9 - 0
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt

@@ -7,6 +7,7 @@ import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.SearchContentHistory
 import io.legado.app.data.repository.BookRepository
 import io.legado.app.data.repository.SearchContentRepository
+import io.legado.app.domain.gateway.ThemeSettingsGateway
 import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.collections.immutable.toImmutableList
@@ -34,6 +35,7 @@ data class SearchContentUiState(
     val searchHistory: ImmutableList<SearchContentHistory> = persistentListOf(),
     val historyOnlyThisBook: Boolean = true,
     val shouldAutoScroll: Boolean = false,
+    val isEInkMode: Boolean = false,
 )
 
 sealed interface SearchContentIntent {
@@ -66,6 +68,7 @@ class SearchContentViewModel(
     private val searchResultIndex: Int,
     private val bookRepository: BookRepository,
     private val searchContentRepository: SearchContentRepository,
+    private val themeSettingsGateway: ThemeSettingsGateway,
 ) : ViewModel() {
     private val restoredSession = if (initialSearchWord == null) {
         searchContentRepository.getLastSession(bookUrl)
@@ -77,6 +80,7 @@ class SearchContentViewModel(
             replaceEnabled = restoredSession?.replaceEnabled ?: false,
             regexReplace = restoredSession?.regexReplace ?: false,
             shouldAutoScroll = searchResultIndex > 0,
+            isEInkMode = themeSettingsGateway.currentSettings.appTheme == "4",
         )
     )
     val uiState = _uiState.asStateFlow()
@@ -90,6 +94,11 @@ class SearchContentViewModel(
 
     init {
         initBook()
+        viewModelScope.launch {
+            themeSettingsGateway.settings.collect { settings ->
+                _uiState.update { it.copy(isEInkMode = settings.appTheme == "4") }
+            }
+        }
     }
 
     fun onIntent(intent: SearchContentIntent) {

+ 79 - 35
app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt

@@ -8,7 +8,13 @@ import android.text.style.BackgroundColorSpan
 import android.text.style.ForegroundColorSpan
 import android.text.style.StyleSpan
 import android.text.style.UnderlineSpan
-import io.legado.app.help.config.AppConfig
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.withStyle
 
 data class SearchResult(
     val bookUrl: String = "",
@@ -27,32 +33,86 @@ data class SearchResult(
     val progressPercent: Float = 0f
 ) {
 
-    fun getTitleSpannable(accentColor: Int): SpannableString {
-        return if (AppConfig.isEInkMode) {
-            SpannableString(chapterTitle).apply {
-                setSpan(UnderlineSpan(), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+    fun getTitleAnnotatedString(
+        accentColor: Color,
+        isEInkMode: Boolean,
+    ): AnnotatedString = buildAnnotatedString {
+        withStyle(
+            SpanStyle(
+                color = if (isEInkMode) Color.Unspecified else accentColor,
+                textDecoration = if (isEInkMode) TextDecoration.Underline else null,
+            )
+        ) {
+            append(chapterTitle)
+        }
+    }
+
+    fun getContentAnnotatedString(
+        textColor: Color,
+        accentColor: Color,
+        backgroundColor: Color,
+        isEInkMode: Boolean,
+    ): AnnotatedString {
+        val ranges = getHighlightRanges()
+
+        return buildAnnotatedString {
+            append(resultText)
+            if (!isEInkMode && resultText.isNotEmpty()) {
+                addStyle(SpanStyle(color = textColor), 0, resultText.length)
             }
-        } else {
-            SpannableString(chapterTitle).apply {
-                setSpan(ForegroundColorSpan(accentColor), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+
+            ranges.forEach { (start, length) ->
+                val end = (start + length).coerceAtMost(resultText.length)
+                if (start !in 0 until end) return@forEach
+                addStyle(
+                    style = if (isEInkMode) {
+                        SpanStyle(textDecoration = TextDecoration.Underline)
+                    } else {
+                        SpanStyle(
+                            color = accentColor,
+                            background = backgroundColor,
+                            fontWeight = FontWeight.Bold,
+                        )
+                    },
+                    start = start,
+                    end = end,
+                )
             }
         }
     }
 
-    fun getContentSpannable(textColor: Int, accentColor: Int, bgColor: Int): SpannableStringBuilder {
-        val spannable = SpannableStringBuilder(resultText)
+    fun getTitleSpannable(accentColor: Int, isEInkMode: Boolean): SpannableString =
+        SpannableString(chapterTitle).apply {
+            if (isEInkMode) {
+                setSpan(UnderlineSpan(), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+            } else {
+                setSpan(ForegroundColorSpan(accentColor), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+            }
+        }
 
-        // Apply the body color first so the more specific highlight color wins.
-        if (!AppConfig.isEInkMode) {
-            spannable.setSpan(
-                ForegroundColorSpan(textColor),
-                0,
-                spannable.length,
-                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
-            )
+    fun getContentSpannable(
+        textColor: Int,
+        accentColor: Int,
+        backgroundColor: Int,
+        isEInkMode: Boolean,
+    ): SpannableStringBuilder = SpannableStringBuilder(resultText).apply {
+        if (!isEInkMode && isNotEmpty()) {
+            setSpan(ForegroundColorSpan(textColor), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
         }
+        getHighlightRanges().forEach { (start, matchLength) ->
+            val end = (start + matchLength).coerceAtMost(resultText.length)
+            if (start !in 0 until end) return@forEach
+            if (isEInkMode) {
+                setSpan(UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+            } else {
+                setSpan(StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+                setSpan(ForegroundColorSpan(accentColor), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+                setSpan(BackgroundColorSpan(backgroundColor), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+            }
+        }
+    }
 
-        val ranges = if (isRegex) {
+    private fun getHighlightRanges(): List<Pair<Int, Int>> = if (isRegex) {
             listOf(queryIndexInResult to matchLength)
         } else if (query.isNotBlank()) {
             buildList {
@@ -67,20 +127,4 @@ data class SearchResult(
         } else {
             emptyList()
         }
-
-        ranges.forEach { (start, length) ->
-            val end = (start + length).coerceAtMost(resultText.length)
-            if (start !in 0 until end) return@forEach
-
-            if (AppConfig.isEInkMode) {
-                spannable.setSpan(UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-            } else {
-                spannable.setSpan(StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-                spannable.setSpan(ForegroundColorSpan(accentColor), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-                spannable.setSpan(BackgroundColorSpan(bgColor), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
-            }
-        }
-
-        return spannable
-    }
 }

+ 14 - 3
app/src/main/java/io/legado/app/ui/widget/components/SearchBar.kt

@@ -24,6 +24,7 @@ import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.unit.dp
@@ -60,15 +61,25 @@ fun SearchBar(
 ) {
     val textFieldState = rememberTextFieldState(initialText = query)
     val focusRequester = remember { FocusRequester() }
+    val focusManager = LocalFocusManager.current
     val keyboardController = LocalSoftwareKeyboardController.current
     var hasFocused by rememberSaveable { mutableStateOf(false) }
 
+    val submitSearch: (String) -> Unit = { value ->
+        onSearch(value)
+        focusManager.clearFocus(force = true)
+        keyboardController?.hide()
+    }
+
     LaunchedEffect(autoFocus) {
         if (autoFocus && !hasFocused) {
             focusRequester.requestFocus()
             // 某些情况下需要手动调用 show() 确保键盘弹出
             keyboardController?.show()
             hasFocused = true
+        } else if (!autoFocus) {
+            focusManager.clearFocus(force = true)
+            keyboardController?.hide()
         }
     }
 
@@ -109,13 +120,13 @@ fun SearchBar(
             trailingIcon = trailingIcon,
             keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
             onKeyboardAction = {
-                onSearch(textFieldState.text.toString())
+                submitSearch(textFieldState.text.toString())
             },
             lineLimits = TextFieldLineLimits.SingleLine,
             backgroundColor = resolvedBackgroundColor,
             miuixUseSearchBarInputField = true,
             miuixSearchBarLabel = resolvedPlaceholder,
-            miuixOnSearch = onSearch,
+            miuixOnSearch = submitSearch,
         )
     } else {
         Surface(
@@ -135,7 +146,7 @@ fun SearchBar(
                 trailingIcon = trailingIcon,
                 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
                 onKeyboardAction = {
-                    onSearch(textFieldState.text.toString())
+                    submitSearch(textFieldState.text.toString())
                 },
                 lineLimits = TextFieldLineLimits.SingleLine,
                 backgroundColor = Color.Transparent

+ 10 - 5
app/src/main/java/io/legado/app/utils/HtmlFormatter.kt

@@ -22,7 +22,10 @@ object HtmlFormatter {
     private val indent2Regex = "^[\\n\\s]+".toRegex()
     private val lastRegex = "[\\n\\s]+$".toRegex()
 
-    fun format(html: String?, otherRegex: Regex = otherHtmlRegex): String {
+    fun format(html: String?, otherRegex: Regex = otherHtmlRegex): String =
+        format(html, otherRegex, "  ")
+
+    private fun format(html: String?, otherRegex: Regex, paragraphIndent: String): String {
         html ?: return ""
         return html.replace(nbspRegex, " ")
             .replace(espRegex, " ")
@@ -30,8 +33,8 @@ object HtmlFormatter {
             .replace(wrapHtmlRegex, "\n")
             .replace(commentRegex, "")
             .replace(otherRegex, "")
-            .replace(indent1Regex, "\n  ")
-            .replace(indent2Regex, "  ")
+            .replace(indent1Regex, "\n$paragraphIndent")
+            .replace(indent2Regex, paragraphIndent)
             .replace(lastRegex, "")
     }
 
@@ -41,9 +44,11 @@ object HtmlFormatter {
      */
     fun formatDisplayText(html: String?): String {
         if (html.isNullOrBlank()) return ""
-        val body = Jsoup.parseBodyFragment(html).body()
+        val document = Jsoup.parseBodyFragment(html)
+        document.outputSettings().prettyPrint(false)
+        val body = document.body()
         body.select("script, style, noscript").remove()
-        return format(body.html())
+        return format(body.html(), otherHtmlRegex, "")
     }
 
     fun formatKeepImg(html: String?, redirectUrl: URL? = null): String {

+ 53 - 0
app/src/test/java/io/legado/app/ui/book/searchContent/SearchResultTest.kt

@@ -0,0 +1,53 @@
+package io.legado.app.ui.book.searchContent
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class SearchResultTest {
+
+    @Test
+    fun `普通搜索高亮结果片段中的全部关键词`() {
+        val accentColor = Color.Red
+        val annotated = SearchResult(
+            resultText = "foo bar foo",
+            query = "foo",
+        ).getContentAnnotatedString(
+            textColor = Color.Black,
+            accentColor = accentColor,
+            backgroundColor = Color.Yellow,
+            isEInkMode = false,
+        )
+
+        val highlights = annotated.spanStyles.filter {
+            it.item.fontWeight == FontWeight.Bold && it.item.color == accentColor
+        }
+
+        assertEquals(listOf(0 to 3, 8 to 11), highlights.map { it.start to it.end })
+    }
+
+    @Test
+    fun `正则搜索使用仓库记录的实际匹配范围`() {
+        val accentColor = Color.Red
+        val annotated = SearchResult(
+            resultText = "abc123def",
+            query = "\\d+",
+            queryIndexInResult = 3,
+            matchLength = 3,
+            isRegex = true,
+        ).getContentAnnotatedString(
+            textColor = Color.Black,
+            accentColor = accentColor,
+            backgroundColor = Color.Yellow,
+            isEInkMode = false,
+        )
+
+        val highlight = annotated.spanStyles.single {
+            it.item.fontWeight == FontWeight.Bold && it.item.color == accentColor
+        }
+
+        assertEquals(3, highlight.start)
+        assertEquals(6, highlight.end)
+    }
+}

+ 9 - 0
app/src/test/java/io/legado/app/utils/HtmlFormatterTest.kt

@@ -18,6 +18,7 @@ class HtmlFormatterTest {
             """.trimIndent()
         )
 
+        assertEquals("第一段\n第二段", result)
         assertTrue(result.contains("第一段"))
         assertTrue(result.contains("第二段"))
         assertFalse(result.contains("color: red"))
@@ -28,4 +29,12 @@ class HtmlFormatterTest {
     fun formatDisplayText_keepsPlainText() {
         assertEquals("普通简介", HtmlFormatter.formatDisplayText("普通简介"))
     }
+
+    @Test
+    fun formatDisplayText_keepsPlainTextLineBreaks() {
+        assertEquals(
+            "第一段\n第二段",
+            HtmlFormatter.formatDisplayText("第一段\n第二段")
+        )
+    }
 }