段评相关

感谢@Luoyacheng,https://github.com/Luoyacheng/legado
This commit is contained in:
HapeLee
2025-09-24 19:24:06 +08:00
parent 53a1a6c89a
commit 891ec2a6ec
7 changed files with 1980 additions and 65 deletions
File diff suppressed because it is too large Load Diff
@@ -9,11 +9,22 @@ object AppPattern {
val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
//匹配格式化后的图片格式 //匹配格式化后的图片格式
val imgPattern: Pattern = val imgPattern: Pattern = Pattern.compile("<img[^>]*src=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>")
Pattern.compile("<img[^>]*src=['\"]([^'\"]*(?:['\"][^>]+\\})?)['\"][^>]*>")
//dataURL图片类型 //dataURL图片类型
val dataUriRegex = Regex("^data:.*?;base64,(.*)") val dataUriRegex = Regex("^data:.*?;base64,(.*)")
//提取标题中的段评
val imgRegex = Regex("(.*)((?:data|https?):[\\s\\S]+)$")
//自定义图片样式
val imgStyRegex = Regex("style[\"'\\s]*:\\s*[\"']([^\"']*)[\"']")
//匹配章节信息中的字数
val wordCountRegex = Regex("(?:^|字数[:、]?|\\s+)([0-9万千百\\.]{1,6}字)")
//正文不计入字数的字符
val noWordCountRegex = Regex("[\\s\\u200B-\\u200F\\uFEFF]")
//提取链接中的域名
val domainRegex = Regex("^https?://([^:/]+)",RegexOption.IGNORE_CASE)
val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著") val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著")
val authorRegex = Regex("^\\s*作\\s*者[:\\s]+|\\s+著") val authorRegex = Regex("^\\s*作\\s*者[:\\s]+|\\s+著")
@@ -55,4 +66,4 @@ object AppPattern {
val regexCharRegex = "[{}()\\[\\].+*?^$\\\\|]".toRegex() val regexCharRegex = "[{}()\\[\\].+*?^$\\\\|]".toRegex()
val LFRegex = "\n".toRegex() val LFRegex = "\n".toRegex()
} }
@@ -65,7 +65,7 @@ val appDb by lazy {
} }
@Database( @Database(
version = 75, version = 76,
exportSchema = true, exportSchema = true,
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
@@ -106,6 +106,7 @@ val appDb by lazy {
AutoMigration(from = 72, to = 73), AutoMigration(from = 72, to = 73),
AutoMigration(from = 73, to = 74), AutoMigration(from = 73, to = 74),
AutoMigration(from = 74, to = 75), AutoMigration(from = 74, to = 75),
AutoMigration(from = 75, to = 76)
] ]
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@@ -55,7 +55,8 @@ data class BookChapter(
var end: Long? = null, // 章节终止位置 var end: Long? = null, // 章节终止位置
var startFragmentId: String? = null, //EPUB书籍当前章节的fragmentId var startFragmentId: String? = null, //EPUB书籍当前章节的fragmentId
var endFragmentId: String? = null, //EPUB书籍下一章节的fragmentId var endFragmentId: String? = null, //EPUB书籍下一章节的fragmentId
var variable: String? = null //变量 var variable: String? = null, //变量
var reviewImg: String? = null //段评图标
) : Parcelable, RuleDataInterface { ) : Parcelable, RuleDataInterface {
@delegate:Transient @delegate:Transient
@@ -63,19 +63,6 @@ object BookContent {
analyzeRule.setChapter(bookChapter) analyzeRule.setChapter(bookChapter)
analyzeRule.setNextChapterUrl(mNextChapterUrl) analyzeRule.setNextChapterUrl(mNextChapterUrl)
coroutineContext.ensureActive() coroutineContext.ensureActive()
val titleRule = contentRule.title
if (!titleRule.isNullOrBlank()) {
val title = analyzeRule.runCatching {
getString(titleRule)
}.onFailure {
Debug.log(bookSource.bookSourceUrl, "获取标题出错, ${it.localizedMessage}")
}.getOrNull()
if (!title.isNullOrBlank()) {
bookChapter.title = title
bookChapter.titleMD5 = null
appDb.bookChapterDao.update(bookChapter)
}
}
var contentData = analyzeContent( var contentData = analyzeContent(
book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl
) )
@@ -135,6 +122,30 @@ object BookContent {
} }
} }
var contentStr = contentList.joinToString("\n") var contentStr = contentList.joinToString("\n")
val titleRule = contentRule.title //先正文再章节名称
if (!titleRule.isNullOrBlank()) {
var title = analyzeRule.runCatching {
getString(titleRule)
}.onFailure {
Debug.log(bookSource.bookSourceUrl, "获取标题出错, ${it.localizedMessage}")
}.getOrNull()
if (!title.isNullOrBlank()) {
val matchResult = AppPattern.imgRegex.find(title)
if (matchResult != null) {
matchResult.groupValues[1]
val (group1,group2) = matchResult.destructured
title = if (group1 != "") {
group1
} else {
bookChapter.title
}
bookChapter.reviewImg = group2
}
bookChapter.title = title
bookChapter.titleMD5 = null
appDb.bookChapterDao.update(bookChapter)
}
}
//全文替换 //全文替换
val replaceRegex = contentRule.replaceRegex val replaceRegex = contentRule.replaceRegex
if (!replaceRegex.isNullOrEmpty()) { if (!replaceRegex.isNullOrEmpty()) {
@@ -46,10 +46,10 @@ import java.util.Locale
@Suppress("DEPRECATION", "ConstPropertyName") @Suppress("DEPRECATION", "ConstPropertyName")
object ChapterProvider { object ChapterProvider {
//用于图片字的替换 //用于图片字的替换
const val srcReplaceChar = "" const val srcReplaceChar = ""//▩ 丨
//用于评论按钮的替换 //用于评论按钮的替换
const val reviewChar = "" const val reviewChar = ""
const val indentChar = " " const val indentChar = " "
@@ -4,8 +4,11 @@ import android.graphics.Paint
import android.text.Layout import android.text.Layout
import android.text.StaticLayout import android.text.StaticLayout
import android.text.TextPaint import android.text.TextPaint
import android.util.Size
import io.legato.kazusa.constant.AppLog import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.AppPattern import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.constant.AppPattern.noWordCountRegex
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.Book import io.legato.kazusa.data.entities.Book
import io.legato.kazusa.data.entities.BookChapter import io.legato.kazusa.data.entities.BookChapter
import io.legato.kazusa.help.book.BookContent import io.legato.kazusa.help.book.BookContent
@@ -21,6 +24,7 @@ import io.legato.kazusa.ui.book.read.page.entities.TextLine
import io.legato.kazusa.ui.book.read.page.entities.TextPage import io.legato.kazusa.ui.book.read.page.entities.TextPage
import io.legato.kazusa.ui.book.read.page.entities.column.ImageColumn import io.legato.kazusa.ui.book.read.page.entities.column.ImageColumn
import io.legato.kazusa.ui.book.read.page.entities.column.TextColumn import io.legato.kazusa.ui.book.read.page.entities.column.TextColumn
import io.legato.kazusa.utils.StringUtils
import io.legato.kazusa.utils.dpToPx import io.legato.kazusa.utils.dpToPx
import io.legato.kazusa.utils.fastSum import io.legato.kazusa.utils.fastSum
import io.legato.kazusa.utils.getTextWidthsCompat import io.legato.kazusa.utils.getTextWidthsCompat
@@ -202,13 +206,25 @@ class TextChapterLayout(
if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) { if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) {
//标题非隐藏 //标题非隐藏
displayTitle.splitNotBlank("\n").forEach { text -> displayTitle.splitNotBlank("\n").forEach { text ->
val srcList = LinkedList<String>()
val reviewImg = bookChapter.reviewImg
var reviewTxt = ""
if (reviewImg != null) {
srcList.add(reviewImg)
reviewTxt = if (reviewImg.contains("TEXT")) {
ChapterProvider.reviewChar
} else {
ChapterProvider.srcReplaceChar
}
}
setTypeText( setTypeText(
book, book,
if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text, text + reviewTxt,
titlePaint, titlePaint,
titlePaintTextHeight, titlePaintTextHeight,
titlePaintFontMetrics, titlePaintFontMetrics,
imageStyle, imageStyle,
srcList = srcList.ifEmpty { null },
isTitle = true, isTitle = true,
emptyContent = contents.isEmpty(), emptyContent = contents.isEmpty(),
isVolumeTitle = bookChapter.isVolume isVolumeTitle = bookChapter.isVolume
@@ -226,11 +242,12 @@ class TextChapterLayout(
val sb = StringBuffer() val sb = StringBuffer()
var isSetTypedImage = false var isSetTypedImage = false
var wordCount = 0
contents.forEach { content -> contents.forEach { content ->
coroutineContext.ensureActive() coroutineContext.ensureActive()
if (isTextImageStyle) { if (isTextImageStyle) {
//图片样式为文字嵌入类型 //图片样式为文字嵌入类型
var text = content.replace(ChapterProvider.srcReplaceChar, "") var text = content.replace(ChapterProvider.srcReplaceChar, "") //▣
val srcList = LinkedList<String>() val srcList = LinkedList<String>()
sb.setLength(0) sb.setLength(0)
val matcher = AppPattern.imgPattern.matcher(text) val matcher = AppPattern.imgPattern.matcher(text)
@@ -242,6 +259,7 @@ class TextChapterLayout(
} }
matcher.appendTail(sb) matcher.appendTail(sb)
text = sb.toString() text = sb.toString()
wordCount += text.replace(noWordCountRegex,"").length
setTypeText( setTypeText(
book, book,
text, text,
@@ -257,29 +275,80 @@ class TextChapterLayout(
prepareNextPageIfNeed() prepareNextPageIfNeed()
} }
var start = 0 var start = 0
val srcList = LinkedList<String>()
sb.setLength(0)
var isFirstLine = true
if (content.contains("<img")) { if (content.contains("<img")) {
val matcher = AppPattern.imgPattern.matcher(content) val text = content.replace(ChapterProvider.srcReplaceChar, "")
val matcher = AppPattern.imgPattern.matcher(text)
while (matcher.find()) { while (matcher.find()) {
coroutineContext.ensureActive() coroutineContext.ensureActive()
val text = content.substring(start, matcher.start()) val imgSrc = matcher.group(1)!!
if (text.isNotBlank()) { var iStyle = imageStyle
setTypeText( var isSmallImage = true
val matchResult = AppPattern.imgStyRegex.find(imgSrc)
var imgSize: Size? = null
if (matchResult != null) {
val styleValue = matchResult.groupValues[1].trim()
if (styleValue == "TEXT" && matcher.end() == text.length) {
iStyle = "TEXT"
wordCount--
}
else if (styleValue.equals("text", true)) {
iStyle = "text"
}
else {
imgSize = ImageProvider.getImageSize(book, imgSrc, ReadBook.bookSource)
iStyle = styleValue
isSmallImage = false
}
}
else {
imgSize = ImageProvider.getImageSize(book, imgSrc, ReadBook.bookSource)
if (imgSize.width < 80 && imgSize.height < 80) {
iStyle = "text"
}
else {
isSmallImage = false
}
}
if (start < matcher.start()) {
sb.append(text.substring(start, matcher.start()))
}
if (isSmallImage) {
sb.append(
if (iStyle == "TEXT")
ChapterProvider.reviewChar
else
ChapterProvider.srcReplaceChar
)
srcList.add(imgSrc)
} else {
val textBefore = sb.toString()
if (textBefore.isNotBlank()) {
wordCount += textBefore.replace(noWordCountRegex,"").length
setTypeText(
book,
sb.toString(),
contentPaint,
contentPaintTextHeight,
contentPaintFontMetrics,
"TEXT",
isFirstLine = isFirstLine,
srcList = srcList
)
sb.setLength(0)
isFirstLine=false
}
setTypeImage(
book, book,
text, imgSrc,
contentPaint,
contentPaintTextHeight, contentPaintTextHeight,
contentPaintFontMetrics, iStyle,
imageStyle, imgSize!!
isFirstLine = start == 0
) )
isSetTypedImage = true
} }
setTypeImage(
book,
matcher.group(1)!!,
contentPaintTextHeight,
imageStyle
)
isSetTypedImage = true
start = matcher.end() start = matcher.end()
} }
} }
@@ -288,23 +357,30 @@ class TextChapterLayout(
isSetTypedImage = false isSetTypedImage = false
prepareNextPageIfNeed() prepareNextPageIfNeed()
} }
val text = content.substring(start, content.length) val textAfter = content.substring(start, content.length)
if (text.isNotBlank()) { sb.append(textAfter)
setTypeText( }
book, val text = sb.toString()
if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text, if (text.isNotBlank()) {
contentPaint, wordCount += text.replace(noWordCountRegex,"").length
contentPaintTextHeight, setTypeText(
contentPaintFontMetrics, book,
imageStyle, if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text,
isFirstLine = start == 0 contentPaint,
) contentPaintTextHeight,
} contentPaintFontMetrics,
"TEXT",
isFirstLine = start == 0,
srcList = srcList.ifEmpty { null }
)
} }
} }
pendingTextPage.lines.last().isParagraphEnd = true pendingTextPage.lines.last().isParagraphEnd = true
stringBuilder.append("\n") stringBuilder.append("\n")
} }
val chapterWordCount = StringUtils.wordCountFormat(wordCount.toString())
bookChapter.wordCount = chapterWordCount
appDb.bookChapterDao.upWordCount(bookChapter.bookUrl, bookChapter.url, chapterWordCount)
val textPage = pendingTextPage val textPage = pendingTextPage
val endPadding = 20.dpToPx() val endPadding = 20.dpToPx()
val durYPadding = durY + endPadding val durYPadding = durY + endPadding
@@ -327,8 +403,8 @@ class TextChapterLayout(
src: String, src: String,
textHeight: Float, textHeight: Float,
imageStyle: String?, imageStyle: String?,
size: Size
) { ) {
val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource)
if (size.width > 0 && size.height > 0) { if (size.width > 0 && size.height > 0) {
prepareNextPageIfNeed(durY) prepareNextPageIfNeed(durY)
var height = size.height var height = size.height
@@ -375,13 +451,19 @@ class TextChapterLayout(
durY += height durY += height
textLine.lineBottom = durY + paddingTop textLine.lineBottom = durY + paddingTop
val (start, end) = if (visibleWidth > width) { val (start, end) = if (visibleWidth > width) {
val adjustWidth = (visibleWidth - width) / 2f when (imageStyle?.uppercase()) {
Pair(adjustWidth, adjustWidth + width) "RIGHT" -> Pair(visibleWidth - width, visibleWidth)
"LEFT" -> Pair(0f, width)
else -> {
val adjustWidth = (visibleWidth - width) / 2f
Pair(adjustWidth, adjustWidth + width)
}
}
} else { } else {
Pair(0f, width.toFloat()) Pair(0f, width)
} }
textLine.addColumn( textLine.addColumn(
ImageColumn(start = absStartX + start, end = absStartX + end, src = src) ImageColumn(start = absStartX + start.toFloat(), end = absStartX + end.toFloat(), src = src)
) )
calcTextLinePosition(textPages, textLine, stringBuilder.length) calcTextLinePosition(textPages, textLine, stringBuilder.length)
stringBuilder.append(" ") // 确保翻页时索引计算正确 stringBuilder.append(" ") // 确保翻页时索引计算正确
@@ -697,7 +779,7 @@ class TextChapterLayout(
srcList: LinkedList<String>? srcList: LinkedList<String>?
) { ) {
val column = when { val column = when {
srcList != null && char == ChapterProvider.srcReplaceChar -> { !srcList.isNullOrEmpty() && (char == ChapterProvider.srcReplaceChar || char == ChapterProvider.reviewChar && isLineEnd) -> {
val src = srcList.removeFirst() val src = srcList.removeFirst()
ImageProvider.cacheImage(book, src, ReadBook.bookSource) ImageProvider.cacheImage(book, src, ReadBook.bookSource)
ImageColumn( ImageColumn(
@@ -706,14 +788,13 @@ class TextChapterLayout(
src = src src = src
) )
} }
/*isLineEnd && char == ChapterProvider.reviewChar -> {
// isLineEnd && char == ChapterProvider.reviewChar -> { ReviewColumn(
// ReviewColumn( start = absStartX + xStart,
// start = absStartX + xStart, end = absStartX + xEnd,
// end = absStartX + xEnd, count = 10
// count = 100 )
// ) }*/
// }
else -> { else -> {
TextColumn( TextColumn(
@@ -820,4 +901,4 @@ class TextChapterLayout(
return code == 8203 || code == 8204 || code == 8205 || code == 8288 return code == 8203 || code == 8204 || code == 8205 || code == 8288
} }
} }