a328929 4 месяцев назад
Родитель
Сommit
bba0f7c015

+ 4 - 1
.github/scripts/lzy_web.py

@@ -1,4 +1,7 @@
-import requests, os, datetime, sys
+import requests
+import os
+import datetime
+import sys
 
 # Cookie 中 phpdisk_info 的值
 cookie_phpdisk_info = os.environ.get('phpdisk_info')

+ 13 - 1
.github/scripts/tg_bot.py

@@ -1,4 +1,16 @@
-import os, sys, telebot
+import datetime
+import os
+import sys
+
+import telebot
+
+
+# 日志打印
+def log(msg):
+    utc_time = datetime.datetime.utcnow()
+    china_time = utc_time + datetime.timedelta(hours=8)
+    print(f"[{china_time.strftime('%Y.%m.%d %H:%M:%S')}] {msg}")
+
 
 # 上传文件
 def upload_file(tb, chat_id, file_dir):

+ 142 - 43
app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt

@@ -35,6 +35,7 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl.Companion.paramPattern
 import io.legado.app.ui.book.read.page.entities.TextChapter
 import io.legado.app.ui.book.read.page.entities.TextLine
 import io.legado.app.ui.book.read.page.entities.TextPage
+import io.legado.app.ui.book.read.page.entities.TitleSegment
 import io.legado.app.ui.book.read.page.entities.column.BaseColumn
 import io.legado.app.ui.book.read.page.entities.column.ImageColumn
 import io.legado.app.ui.book.read.page.entities.column.TextBaseColumn
@@ -107,6 +108,8 @@ class TextChapterLayout(
     private val highlightRuleRepository: HighlightRuleRepository
         get() = GlobalContext.get().get()
 
+    private var highlightStyleContext: HighlightStyleContext? = null
+
     @Volatile
     private var listener: LayoutProgressListener? = textChapter
 
@@ -283,7 +286,10 @@ class TextChapterLayout(
             }
         } else null
 
-        var currentOffset = 0
+        highlightStyleContext = buildHighlightStyleContext(allTitleSegments, contents)
+
+        var titleHighlightOffset = 0
+        var bodyHighlightOffset = 0
 
         if (allTitleSegments != null) {
             allTitleSegments.forEachIndexed { index, segment ->
@@ -323,9 +329,9 @@ class TextChapterLayout(
                     isTitle = true,
                     emptyContent = contents.isEmpty(),
                     isVolumeTitle = bookChapter.isVolume,
-                    offset = currentOffset
+                    offset = titleHighlightOffset
                 )
-                currentOffset += text.length + 1
+                titleHighlightOffset += segment.text.length + 1
 
                 if (segment.scale != 1.0f) {
                     val currentLines = pendingTextPage.lines
@@ -356,11 +362,9 @@ class TextChapterLayout(
                 val text = content.trim()
                 if (text == "[newpage]") {
                     prepareNextPageIfNeed()
-                    currentOffset += content.length + 1
                     return@forEach
                 } else if (text.startsWith("<usehtml>")) {
                     setTypeHtml(imageStyle, book, text.substring(9, text.lastIndexOf("<")))
-                    currentOffset += content.length + 1
                     return@forEach
                 }
             }
@@ -387,9 +391,9 @@ class TextChapterLayout(
                     contentPaintFontMetrics,
                     imageStyle,
                     srcList = srcList,
-                    offset = currentOffset
+                    offset = bodyHighlightOffset
                 )
-                currentOffset += text.length
+                bodyHighlightOffset += text.length
             } else {
                 if (isSingleImageStyle && isSetTypedImage) {
                     isSetTypedImage = false
@@ -459,9 +463,9 @@ class TextChapterLayout(
                                     book, textBefore, contentPaint, contentPaintTextHeight,
                                     contentPaintFontMetrics, "TEXT", isFirstLine = isFirstLine,
                                     srcList = srcList, clickList = clickList,
-                                    offset = currentOffset
+                                    offset = bodyHighlightOffset
                                 )
-                                currentOffset += textBefore.length
+                                bodyHighlightOffset += textBefore.length
                                 sb.setLength(0)
                                 isFirstLine = false
                             }
@@ -473,7 +477,7 @@ class TextChapterLayout(
                                 imgSize,
                                 click
                             ) // 传递点击信息
-                            currentOffset += 1
+                            bodyHighlightOffset += 1
                             isSetTypedImage = true
                         }
                         start = matcher.end()
@@ -501,14 +505,14 @@ class TextChapterLayout(
                         isFirstLine = isFirstLine,
                         srcList = srcList.ifEmpty { null },
                         clickList = clickList.ifEmpty { null },
-                        offset = currentOffset
+                        offset = bodyHighlightOffset
                     )
-                    currentOffset += textToType.length
+                    bodyHighlightOffset += text.length
                 }
             }
             pendingTextPage.lines.last().isParagraphEnd = true
             stringBuilder.append("\n")
-            currentOffset += 1
+            bodyHighlightOffset += 1
         }
         val chapterWordCount = StringUtils.wordCountFormat(wordCount.toString())
         bookChapter.wordCount = chapterWordCount
@@ -921,7 +925,7 @@ class TextChapterLayout(
         clickList: LinkedList<String?>? = null,
         offset: Int = -1
     ) {
-        val charStyles = applyHighlightRules(text, isTitle)
+        val charStyles = applyHighlightRules(text, isTitle, offset)
         val widthsArray = allocateFloatArray(text.length)
         textPaint.getTextWidthsCompat(text, widthsArray)
         // 用高亮规则字体重新测量字符宽度,确保排版和绘制使用同一套字体
@@ -1397,47 +1401,142 @@ class TextChapterLayout(
      */
     private fun applyHighlightRules(
         text: String,
-        isTitle: Boolean = false
+        isTitle: Boolean = false,
+        offset: Int = -1
     ): Array<CharStyle?>? {
-        if (compiledHighlightRules.isEmpty()) return null
-        var hasMatch = false
-        // 先检查是否有任何规则匹配
-        for (compiled in compiledHighlightRules) {
-            if (!compiled.rule.appliesTo(isTitle)) continue
-            if (compiled.regex.containsMatchIn(text)) {
-                hasMatch = true
-                break
+        if (offset >= 0) {
+            return highlightStyleContext?.stylesFor(isTitle, offset, text.length)
+        }
+        return createHighlightStyles(text, isTitle, compiledHighlightRules)
+    }
+
+    private fun buildHighlightStyleContext(
+        titleSegments: List<TitleSegment>?,
+        contents: List<String>,
+    ): HighlightStyleContext? {
+        val rules = compiledHighlightRules
+        if (rules.isEmpty()) return null
+        val titleStyles = createHighlightStyles(
+            text = buildTitleHighlightText(titleSegments),
+            isTitle = true,
+            rules = rules
+        )
+        val bodyStyles = createHighlightStyles(
+            text = buildBodyHighlightText(contents),
+            isTitle = false,
+            rules = rules
+        )
+        if (titleStyles == null && bodyStyles == null) return null
+        return HighlightStyleContext(titleStyles, bodyStyles)
+    }
+
+    private fun buildTitleHighlightText(titleSegments: List<TitleSegment>?): String {
+        if (titleSegments.isNullOrEmpty()) return ""
+        return buildString {
+            titleSegments.forEach { segment ->
+                append(segment.text)
+                append('\n')
+            }
+        }
+    }
+
+    private fun buildBodyHighlightText(contents: List<String>): String {
+        if (contents.isEmpty()) return ""
+        return buildString {
+            contents.forEach { content ->
+                if (adaptSpecialStyle) {
+                    val text = content.trim()
+                    if (text == "[newpage]" || text.startsWith("<usehtml>")) {
+                        return@forEach
+                    }
+                }
+                append(
+                    content.replace(srcReplaceCharC, srcReplaceCharD)
+                        .replaceImagesForHighlight()
+                )
+                append('\n')
             }
         }
-        if (!hasMatch) return null
-        // 填充样式数组
-        val styles = arrayOfNulls<CharStyle>(text.length)
-        for (compiled in compiledHighlightRules) {
+    }
+
+    private fun String.replaceImagesForHighlight(): String {
+        val matcher = AppPattern.imgPattern.matcher(this)
+        if (!matcher.find()) return this
+        return buildString(length) {
+            var start = 0
+            do {
+                append(this@replaceImagesForHighlight, start, matcher.start())
+                append(srcReplaceChar)
+                start = matcher.end()
+            } while (matcher.find())
+            append(this@replaceImagesForHighlight, start, this@replaceImagesForHighlight.length)
+        }
+    }
+
+    private fun createHighlightStyles(
+        text: String,
+        isTitle: Boolean,
+        rules: List<CompiledHighlightRule>,
+    ): Array<CharStyle?>? {
+        if (text.isEmpty() || rules.isEmpty()) return null
+        var styles: Array<CharStyle?>? = null
+        for (compiled in rules) {
             if (!compiled.rule.appliesTo(isTitle)) continue
-            val rule = compiled.rule
-            val charStyle = CharStyle(
-                textColor = rule.textColor,
-                bgColor = rule.bgColor,
-                underlineMode = rule.underlineMode,
-                underlineColor = rule.underlineColor ?: rule.textColor ?: 0xFF63C37D.toInt(),
-                underlineWidth = rule.underlineWidth,
-                underlineOffset = rule.underlineOffset,
-                underlineSvgPath = rule.underlineSvgPath.orEmpty(),
-                bgImage = rule.bgImage.orEmpty(),
-                bgImageFit = rule.bgImageFit,
-                bgImageScale = rule.bgImageScale,
-                fontPath = rule.fontPath.orEmpty()
-            )
+            val charStyle = compiled.rule.toCharStyle()
             compiled.regex.findAll(text).forEach { match ->
-                for (i in match.range) {
+                val start = match.range.first.coerceAtLeast(0)
+                val end = match.range.last.coerceAtMost(text.lastIndex)
+                if (start > end) return@forEach
+                val activeStyles = styles ?: arrayOfNulls<CharStyle>(text.length).also {
+                    styles = it
+                }
+                for (i in start..end) {
                     // 后来的规则覆盖先前的(与 Legado_Max 行为一致)
-                    styles[i] = charStyle
+                    activeStyles[i] = charStyle
                 }
             }
         }
         return styles
     }
 
+    private fun HighlightRule.toCharStyle(): CharStyle {
+        return CharStyle(
+            textColor = textColor,
+            bgColor = bgColor,
+            underlineMode = underlineMode,
+            underlineColor = underlineColor ?: textColor ?: 0xFF63C37D.toInt(),
+            underlineWidth = underlineWidth,
+            underlineOffset = underlineOffset,
+            underlineSvgPath = underlineSvgPath.orEmpty(),
+            bgImage = bgImage.orEmpty(),
+            bgImageFit = bgImageFit,
+            bgImageScale = bgImageScale,
+            fontPath = fontPath.orEmpty()
+        )
+    }
+
+    private data class HighlightStyleContext(
+        val titleStyles: Array<CharStyle?>?,
+        val bodyStyles: Array<CharStyle?>?,
+    ) {
+        fun stylesFor(isTitle: Boolean, offset: Int, length: Int): Array<CharStyle?>? {
+            if (length <= 0) return null
+            val source = if (isTitle) titleStyles else bodyStyles
+            if (source == null || offset !in source.indices) return null
+            val copyLength = length.coerceAtMost(source.size - offset)
+            if (copyLength <= 0) return null
+            var hasStyle = false
+            val result = arrayOfNulls<CharStyle>(length)
+            for (i in 0 until copyLength) {
+                source[offset + i]?.let { style ->
+                    result[i] = style
+                    hasStyle = true
+                }
+            }
+            return if (hasStyle) result else null
+        }
+    }
+
     private data class CompiledHighlightRule(
         val rule: HighlightRule,
         val regex: Regex,