Merge branch 'main' of https://github.com/HapeLee/legado-with-MD3
This commit is contained in:
@@ -134,6 +134,8 @@ class TextChapterLayout(
|
|||||||
|
|
||||||
var channel = Channel<TextPage>(Channel.UNLIMITED)
|
var channel = Channel<TextPage>(Channel.UNLIMITED)
|
||||||
|
|
||||||
|
private var globalRegexResult: RegexMatchResult? = null
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
job = Coroutine.async(
|
job = Coroutine.async(
|
||||||
@@ -241,8 +243,8 @@ class TextChapterLayout(
|
|||||||
val isSingleImageStyle = imageStyle.equals(Book.imgStyleSingle, true)
|
val isSingleImageStyle = imageStyle.equals(Book.imgStyleSingle, true)
|
||||||
val isTextImageStyle = imageStyle.equals(Book.imgStyleText, true)
|
val isTextImageStyle = imageStyle.equals(Book.imgStyleText, true)
|
||||||
|
|
||||||
if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) {
|
val allTitleSegments = if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) {
|
||||||
val allTitleSegments = displayTitle.splitNotBlank("\n").flatMap { rawTitle ->
|
displayTitle.splitNotBlank("\n").flatMap { rawTitle ->
|
||||||
TitleStyleParser.getSegments(
|
TitleStyleParser.getSegments(
|
||||||
rawTitle,
|
rawTitle,
|
||||||
titleSegType,
|
titleSegType,
|
||||||
@@ -251,7 +253,66 @@ class TextChapterLayout(
|
|||||||
titleSegScaling
|
titleSegScaling
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else null
|
||||||
|
|
||||||
|
if (ReadBookConfig.regexColorRules.isNotEmpty()) {
|
||||||
|
val fullTextBuilder = StringBuilder()
|
||||||
|
allTitleSegments?.forEachIndexed { index, segment ->
|
||||||
|
val reviewImg = bookChapter.reviewImg
|
||||||
|
var reviewTxt = ""
|
||||||
|
if (index == allTitleSegments.lastIndex && reviewImg != null) {
|
||||||
|
reviewTxt = if (reviewImg.contains("TEXT")) reviewChar else srcReplaceChar
|
||||||
|
}
|
||||||
|
fullTextBuilder.append(segment.text).append(reviewTxt).append("\n")
|
||||||
|
}
|
||||||
|
contents.forEach { content ->
|
||||||
|
if (adaptSpecialStyle) {
|
||||||
|
val t = content.trim()
|
||||||
|
if (t == "[newpage]" || t.startsWith("<usehtml>")) {
|
||||||
|
fullTextBuilder.append(content).append("\n")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val text = content.replace(srcReplaceCharC, srcReplaceCharD)
|
||||||
|
if (isTextImageStyle) {
|
||||||
|
val matcher = AppPattern.imgPattern.matcher(text)
|
||||||
|
val ssb = StringBuffer()
|
||||||
|
while (matcher.find()) {
|
||||||
|
if (matcher.group(1) != null) {
|
||||||
|
matcher.appendReplacement(ssb, srcReplaceChar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matcher.appendTail(ssb)
|
||||||
|
fullTextBuilder.append(ssb.toString())
|
||||||
|
} else {
|
||||||
|
val matcher = AppPattern.imgPattern.matcher(text)
|
||||||
|
var start = 0
|
||||||
|
while (matcher.find()) {
|
||||||
|
val imgSrc = matcher.group(1) ?: continue
|
||||||
|
val iStyle = if (imgSrc.contains("TEXT")) "text" else imageStyle
|
||||||
|
if (start < matcher.start()) {
|
||||||
|
fullTextBuilder.append(text.substring(start, matcher.start()))
|
||||||
|
}
|
||||||
|
if (iStyle == "text" || iStyle == "TEXT") {
|
||||||
|
fullTextBuilder.append(if (iStyle == "TEXT") reviewChar else srcReplaceChar)
|
||||||
|
} else {
|
||||||
|
fullTextBuilder.append(" ")
|
||||||
|
}
|
||||||
|
start = matcher.end()
|
||||||
|
}
|
||||||
|
if (start < text.length) {
|
||||||
|
fullTextBuilder.append(text.substring(start))
|
||||||
|
}
|
||||||
|
if (AppConfig.enableReview) fullTextBuilder.append(reviewChar)
|
||||||
|
}
|
||||||
|
fullTextBuilder.append("\n")
|
||||||
|
}
|
||||||
|
preApplyRegexColorRules(fullTextBuilder.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentOffset = 0
|
||||||
|
|
||||||
|
if (allTitleSegments != null) {
|
||||||
allTitleSegments.forEachIndexed { index, segment ->
|
allTitleSegments.forEachIndexed { index, segment ->
|
||||||
val currentPaint: TextPaint
|
val currentPaint: TextPaint
|
||||||
val currentHeight: Float
|
val currentHeight: Float
|
||||||
@@ -277,9 +338,10 @@ class TextChapterLayout(
|
|||||||
reviewTxt = if (reviewImg.contains("TEXT")) reviewChar else srcReplaceChar
|
reviewTxt = if (reviewImg.contains("TEXT")) reviewChar else srcReplaceChar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val text = segment.text + reviewTxt
|
||||||
setTypeText(
|
setTypeText(
|
||||||
book = book,
|
book = book,
|
||||||
text = segment.text + reviewTxt,
|
text = text,
|
||||||
textPaint = currentPaint,
|
textPaint = currentPaint,
|
||||||
textHeight = currentHeight,
|
textHeight = currentHeight,
|
||||||
fontMetrics = currentMetrics,
|
fontMetrics = currentMetrics,
|
||||||
@@ -287,8 +349,10 @@ class TextChapterLayout(
|
|||||||
srcList = srcList.ifEmpty { null },
|
srcList = srcList.ifEmpty { null },
|
||||||
isTitle = true,
|
isTitle = true,
|
||||||
emptyContent = contents.isEmpty(),
|
emptyContent = contents.isEmpty(),
|
||||||
isVolumeTitle = bookChapter.isVolume
|
isVolumeTitle = bookChapter.isVolume,
|
||||||
|
offset = currentOffset
|
||||||
)
|
)
|
||||||
|
currentOffset += text.length + 1
|
||||||
|
|
||||||
if (segment.scale != 1.0f) {
|
if (segment.scale != 1.0f) {
|
||||||
val currentLines = pendingTextPage.lines
|
val currentLines = pendingTextPage.lines
|
||||||
@@ -319,9 +383,11 @@ class TextChapterLayout(
|
|||||||
val text = content.trim()
|
val text = content.trim()
|
||||||
if (text == "[newpage]") {
|
if (text == "[newpage]") {
|
||||||
prepareNextPageIfNeed()
|
prepareNextPageIfNeed()
|
||||||
|
currentOffset += content.length + 1
|
||||||
return@forEach
|
return@forEach
|
||||||
} else if (text.startsWith("<usehtml>")) {
|
} else if (text.startsWith("<usehtml>")) {
|
||||||
setTypeHtml(imageStyle, book, text.substring(9, text.lastIndexOf("<")))
|
setTypeHtml(imageStyle, book, text.substring(9, text.lastIndexOf("<")))
|
||||||
|
currentOffset += content.length + 1
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,8 +413,10 @@ class TextChapterLayout(
|
|||||||
contentPaintTextHeight,
|
contentPaintTextHeight,
|
||||||
contentPaintFontMetrics,
|
contentPaintFontMetrics,
|
||||||
imageStyle,
|
imageStyle,
|
||||||
srcList = srcList
|
srcList = srcList,
|
||||||
|
offset = currentOffset
|
||||||
)
|
)
|
||||||
|
currentOffset += text.length
|
||||||
} else {
|
} else {
|
||||||
if (isSingleImageStyle && isSetTypedImage) {
|
if (isSingleImageStyle && isSetTypedImage) {
|
||||||
isSetTypedImage = false
|
isSetTypedImage = false
|
||||||
@@ -402,10 +470,12 @@ class TextChapterLayout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (start < matcher.start()) {
|
if (start < matcher.start()) {
|
||||||
sb.append(text.substring(start, matcher.start()))
|
val textPart = text.substring(start, matcher.start())
|
||||||
|
sb.append(textPart)
|
||||||
}
|
}
|
||||||
if (iStyle == "text" || iStyle == "TEXT") {
|
if (iStyle == "text" || iStyle == "TEXT") {
|
||||||
sb.append(if (iStyle == "TEXT") reviewChar else srcReplaceChar)
|
val charPart = if (iStyle == "TEXT") reviewChar else srcReplaceChar
|
||||||
|
sb.append(charPart)
|
||||||
srcList.add(imgSrc)
|
srcList.add(imgSrc)
|
||||||
clickList.add(click)
|
clickList.add(click)
|
||||||
} else {
|
} else {
|
||||||
@@ -413,10 +483,12 @@ class TextChapterLayout(
|
|||||||
if (textBefore.isNotBlank()) {
|
if (textBefore.isNotBlank()) {
|
||||||
wordCount += textBefore.replace(noWordCountRegex,"").length
|
wordCount += textBefore.replace(noWordCountRegex,"").length
|
||||||
setTypeText(
|
setTypeText(
|
||||||
book, sb.toString(), contentPaint, contentPaintTextHeight,
|
book, textBefore, contentPaint, contentPaintTextHeight,
|
||||||
contentPaintFontMetrics, "TEXT", isFirstLine = isFirstLine,
|
contentPaintFontMetrics, "TEXT", isFirstLine = isFirstLine,
|
||||||
srcList = srcList, clickList = clickList
|
srcList = srcList, clickList = clickList,
|
||||||
|
offset = currentOffset
|
||||||
)
|
)
|
||||||
|
currentOffset += textBefore.length
|
||||||
sb.setLength(0)
|
sb.setLength(0)
|
||||||
isFirstLine = false
|
isFirstLine = false
|
||||||
}
|
}
|
||||||
@@ -428,6 +500,7 @@ class TextChapterLayout(
|
|||||||
imgSize,
|
imgSize,
|
||||||
click
|
click
|
||||||
) // 传递点击信息
|
) // 传递点击信息
|
||||||
|
currentOffset += 1
|
||||||
isSetTypedImage = true
|
isSetTypedImage = true
|
||||||
}
|
}
|
||||||
start = matcher.end()
|
start = matcher.end()
|
||||||
@@ -444,21 +517,25 @@ class TextChapterLayout(
|
|||||||
text = sb.toString()
|
text = sb.toString()
|
||||||
if (text.isNotBlank()) {
|
if (text.isNotBlank()) {
|
||||||
wordCount += text.replace(noWordCountRegex,"").length
|
wordCount += text.replace(noWordCountRegex,"").length
|
||||||
|
val textToType = if (AppConfig.enableReview) text + reviewChar else text
|
||||||
setTypeText(
|
setTypeText(
|
||||||
book,
|
book,
|
||||||
if (AppConfig.enableReview) text + reviewChar else text,
|
textToType,
|
||||||
contentPaint,
|
contentPaint,
|
||||||
contentPaintTextHeight,
|
contentPaintTextHeight,
|
||||||
contentPaintFontMetrics,
|
contentPaintFontMetrics,
|
||||||
"TEXT",
|
"TEXT",
|
||||||
isFirstLine = isFirstLine,
|
isFirstLine = isFirstLine,
|
||||||
srcList = srcList.ifEmpty { null },
|
srcList = srcList.ifEmpty { null },
|
||||||
clickList = clickList.ifEmpty { null }
|
clickList = clickList.ifEmpty { null },
|
||||||
|
offset = currentOffset
|
||||||
)
|
)
|
||||||
|
currentOffset += textToType.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pendingTextPage.lines.last().isParagraphEnd = true
|
pendingTextPage.lines.last().isParagraphEnd = true
|
||||||
stringBuilder.append("\n")
|
stringBuilder.append("\n")
|
||||||
|
currentOffset += 1
|
||||||
}
|
}
|
||||||
val chapterWordCount = StringUtils.wordCountFormat(wordCount.toString())
|
val chapterWordCount = StringUtils.wordCountFormat(wordCount.toString())
|
||||||
bookChapter.wordCount = chapterWordCount
|
bookChapter.wordCount = chapterWordCount
|
||||||
@@ -868,11 +945,12 @@ class TextChapterLayout(
|
|||||||
emptyContent: Boolean = false,
|
emptyContent: Boolean = false,
|
||||||
isVolumeTitle: Boolean = false,
|
isVolumeTitle: Boolean = false,
|
||||||
srcList: LinkedList<String>? = null,
|
srcList: LinkedList<String>? = null,
|
||||||
clickList: LinkedList<String?>? = null
|
clickList: LinkedList<String?>? = null,
|
||||||
|
offset: Int = -1
|
||||||
) {
|
) {
|
||||||
val widthsArray = allocateFloatArray(text.length)
|
val widthsArray = allocateFloatArray(text.length)
|
||||||
textPaint.getTextWidthsCompat(text, widthsArray)
|
textPaint.getTextWidthsCompat(text, widthsArray)
|
||||||
val colorMap = applyRegexColorRules(text)
|
val colorMap = applyRegexColorRules(text, offset)
|
||||||
val layout = if (useZhLayout) {
|
val layout = if (useZhLayout) {
|
||||||
val (words, widths) = measureTextSplit(text, widthsArray)
|
val (words, widths) = measureTextSplit(text, widthsArray)
|
||||||
val indentSize = if (isFirstLine) paragraphIndent.length else 0
|
val indentSize = if (isFirstLine) paragraphIndent.length else 0
|
||||||
@@ -1310,28 +1388,21 @@ class TextChapterLayout(
|
|||||||
val fontPathArray: Array<String?>
|
val fontPathArray: Array<String?>
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun applyRegexColorRules(text: String): RegexMatchResult? {
|
private fun preApplyRegexColorRules(fullText: String) {
|
||||||
val rules = ReadBookConfig.regexColorRules
|
val rules = ReadBookConfig.regexColorRules
|
||||||
if (rules.isEmpty()) return null
|
if (rules.isEmpty()) return
|
||||||
|
val colorArray = IntArray(fullText.length) { -1 }
|
||||||
|
val fontPathArray = arrayOfNulls<String>(fullText.length)
|
||||||
var hasMatch = false
|
var hasMatch = false
|
||||||
for (rule in rules) {
|
for (rule in rules) {
|
||||||
try {
|
try {
|
||||||
val regex = regexCache.getOrPut(rule.pattern) { Regex(rule.pattern) }
|
val regex = synchronized(regexCache) {
|
||||||
if (regex.containsMatchIn(text)) {
|
regexCache.getOrPut(rule.pattern) {
|
||||||
hasMatch = true
|
Regex(rule.pattern, RegexOption.DOT_MATCHES_ALL)
|
||||||
break
|
}
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
regex.findAll(fullText).forEach { match ->
|
||||||
}
|
hasMatch = true
|
||||||
}
|
|
||||||
if (!hasMatch) return null
|
|
||||||
val colorArray = IntArray(text.length) { -1 }
|
|
||||||
val fontPathArray = arrayOfNulls<String>(text.length)
|
|
||||||
for (rule in rules) {
|
|
||||||
try {
|
|
||||||
val regex = regexCache.getOrPut(rule.pattern) { Regex(rule.pattern) }
|
|
||||||
val matches = regex.findAll(text)
|
|
||||||
for (match in matches) {
|
|
||||||
for (i in match.range) {
|
for (i in match.range) {
|
||||||
colorArray[i] = rule.color
|
colorArray[i] = rule.color
|
||||||
if (rule.fontPath.isNotEmpty()) {
|
if (rule.fontPath.isNotEmpty()) {
|
||||||
@@ -1342,6 +1413,34 @@ class TextChapterLayout(
|
|||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (hasMatch) globalRegexResult = RegexMatchResult(colorArray, fontPathArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyRegexColorRules(text: String, offset: Int): RegexMatchResult? {
|
||||||
|
val globalResult = globalRegexResult ?: return null
|
||||||
|
if (offset < 0) return null
|
||||||
|
val endIdx = minOf(offset + text.length, globalResult.colorArray.size)
|
||||||
|
var hasMatch = false
|
||||||
|
for (globalIdx in offset until endIdx) {
|
||||||
|
if (globalResult.colorArray[globalIdx] != -1 || globalResult.fontPathArray[globalIdx] != null) {
|
||||||
|
hasMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasMatch) return null
|
||||||
|
val colorArray = IntArray(text.length) { -1 }
|
||||||
|
val fontPathArray = arrayOfNulls<String>(text.length)
|
||||||
|
for (i in text.indices) {
|
||||||
|
val globalIdx = offset + i
|
||||||
|
if (globalIdx >= 0 && globalIdx < globalResult.colorArray.size) {
|
||||||
|
if (globalResult.colorArray[globalIdx] != -1) {
|
||||||
|
colorArray[i] = globalResult.colorArray[globalIdx]
|
||||||
|
}
|
||||||
|
if (globalResult.fontPathArray[globalIdx] != null) {
|
||||||
|
fontPathArray[i] = globalResult.fontPathArray[globalIdx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return RegexMatchResult(colorArray, fontPathArray)
|
return RegexMatchResult(colorArray, fontPathArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user