段评相关

感谢@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 imgPattern: Pattern =
Pattern.compile("<img[^>]*src=['\"]([^'\"]*(?:['\"][^>]+\\})?)['\"][^>]*>")
val imgPattern: Pattern = Pattern.compile("<img[^>]*src=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>")
//dataURL图片类型
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 authorRegex = Regex("^\\s*作\\s*者[:\\s]+|\\s+著")
@@ -55,4 +66,4 @@ object AppPattern {
val regexCharRegex = "[{}()\\[\\].+*?^$\\\\|]".toRegex()
val LFRegex = "\n".toRegex()
}
}
@@ -65,7 +65,7 @@ val appDb by lazy {
}
@Database(
version = 75,
version = 76,
exportSchema = true,
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
@@ -106,6 +106,7 @@ val appDb by lazy {
AutoMigration(from = 72, to = 73),
AutoMigration(from = 73, to = 74),
AutoMigration(from = 74, to = 75),
AutoMigration(from = 75, to = 76)
]
)
abstract class AppDatabase : RoomDatabase() {
@@ -55,7 +55,8 @@ data class BookChapter(
var end: Long? = null, // 章节终止位置
var startFragmentId: String? = null, //EPUB书籍当前章节的fragmentId
var endFragmentId: String? = null, //EPUB书籍下一章节的fragmentId
var variable: String? = null //变量
var variable: String? = null, //变量
var reviewImg: String? = null //段评图标
) : Parcelable, RuleDataInterface {
@delegate:Transient
@@ -63,19 +63,6 @@ object BookContent {
analyzeRule.setChapter(bookChapter)
analyzeRule.setNextChapterUrl(mNextChapterUrl)
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(
book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl
)
@@ -135,6 +122,30 @@ object BookContent {
}
}
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
if (!replaceRegex.isNullOrEmpty()) {
@@ -46,10 +46,10 @@ import java.util.Locale
@Suppress("DEPRECATION", "ConstPropertyName")
object ChapterProvider {
//用于图片字的替换
const val srcReplaceChar = ""
const val srcReplaceChar = ""//▩ 丨
//用于评论按钮的替换
const val reviewChar = ""
const val reviewChar = ""
const val indentChar = " "
@@ -4,8 +4,11 @@ import android.graphics.Paint
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.util.Size
import io.legato.kazusa.constant.AppLog
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.BookChapter
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.column.ImageColumn
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.fastSum
import io.legato.kazusa.utils.getTextWidthsCompat
@@ -202,13 +206,25 @@ class TextChapterLayout(
if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) {
//标题非隐藏
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(
book,
if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text,
text + reviewTxt,
titlePaint,
titlePaintTextHeight,
titlePaintFontMetrics,
imageStyle,
srcList = srcList.ifEmpty { null },
isTitle = true,
emptyContent = contents.isEmpty(),
isVolumeTitle = bookChapter.isVolume
@@ -226,11 +242,12 @@ class TextChapterLayout(
val sb = StringBuffer()
var isSetTypedImage = false
var wordCount = 0
contents.forEach { content ->
coroutineContext.ensureActive()
if (isTextImageStyle) {
//图片样式为文字嵌入类型
var text = content.replace(ChapterProvider.srcReplaceChar, "")
var text = content.replace(ChapterProvider.srcReplaceChar, "") //▣
val srcList = LinkedList<String>()
sb.setLength(0)
val matcher = AppPattern.imgPattern.matcher(text)
@@ -242,6 +259,7 @@ class TextChapterLayout(
}
matcher.appendTail(sb)
text = sb.toString()
wordCount += text.replace(noWordCountRegex,"").length
setTypeText(
book,
text,
@@ -257,29 +275,80 @@ class TextChapterLayout(
prepareNextPageIfNeed()
}
var start = 0
val srcList = LinkedList<String>()
sb.setLength(0)
var isFirstLine = true
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()) {
coroutineContext.ensureActive()
val text = content.substring(start, matcher.start())
if (text.isNotBlank()) {
setTypeText(
val imgSrc = matcher.group(1)!!
var iStyle = imageStyle
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,
text,
contentPaint,
imgSrc,
contentPaintTextHeight,
contentPaintFontMetrics,
imageStyle,
isFirstLine = start == 0
iStyle,
imgSize!!
)
isSetTypedImage = true
}
setTypeImage(
book,
matcher.group(1)!!,
contentPaintTextHeight,
imageStyle
)
isSetTypedImage = true
start = matcher.end()
}
}
@@ -288,23 +357,30 @@ class TextChapterLayout(
isSetTypedImage = false
prepareNextPageIfNeed()
}
val text = content.substring(start, content.length)
if (text.isNotBlank()) {
setTypeText(
book,
if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text,
contentPaint,
contentPaintTextHeight,
contentPaintFontMetrics,
imageStyle,
isFirstLine = start == 0
)
}
val textAfter = content.substring(start, content.length)
sb.append(textAfter)
}
val text = sb.toString()
if (text.isNotBlank()) {
wordCount += text.replace(noWordCountRegex,"").length
setTypeText(
book,
if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text,
contentPaint,
contentPaintTextHeight,
contentPaintFontMetrics,
"TEXT",
isFirstLine = start == 0,
srcList = srcList.ifEmpty { null }
)
}
}
pendingTextPage.lines.last().isParagraphEnd = true
stringBuilder.append("\n")
}
val chapterWordCount = StringUtils.wordCountFormat(wordCount.toString())
bookChapter.wordCount = chapterWordCount
appDb.bookChapterDao.upWordCount(bookChapter.bookUrl, bookChapter.url, chapterWordCount)
val textPage = pendingTextPage
val endPadding = 20.dpToPx()
val durYPadding = durY + endPadding
@@ -327,8 +403,8 @@ class TextChapterLayout(
src: String,
textHeight: Float,
imageStyle: String?,
size: Size
) {
val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource)
if (size.width > 0 && size.height > 0) {
prepareNextPageIfNeed(durY)
var height = size.height
@@ -375,13 +451,19 @@ class TextChapterLayout(
durY += height
textLine.lineBottom = durY + paddingTop
val (start, end) = if (visibleWidth > width) {
val adjustWidth = (visibleWidth - width) / 2f
Pair(adjustWidth, adjustWidth + width)
when (imageStyle?.uppercase()) {
"RIGHT" -> Pair(visibleWidth - width, visibleWidth)
"LEFT" -> Pair(0f, width)
else -> {
val adjustWidth = (visibleWidth - width) / 2f
Pair(adjustWidth, adjustWidth + width)
}
}
} else {
Pair(0f, width.toFloat())
Pair(0f, width)
}
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)
stringBuilder.append(" ") // 确保翻页时索引计算正确
@@ -697,7 +779,7 @@ class TextChapterLayout(
srcList: LinkedList<String>?
) {
val column = when {
srcList != null && char == ChapterProvider.srcReplaceChar -> {
!srcList.isNullOrEmpty() && (char == ChapterProvider.srcReplaceChar || char == ChapterProvider.reviewChar && isLineEnd) -> {
val src = srcList.removeFirst()
ImageProvider.cacheImage(book, src, ReadBook.bookSource)
ImageColumn(
@@ -706,14 +788,13 @@ class TextChapterLayout(
src = src
)
}
// isLineEnd && char == ChapterProvider.reviewChar -> {
// ReviewColumn(
// start = absStartX + xStart,
// end = absStartX + xEnd,
// count = 100
// )
// }
/*isLineEnd && char == ChapterProvider.reviewChar -> {
ReviewColumn(
start = absStartX + xStart,
end = absStartX + xEnd,
count = 10
)
}*/
else -> {
TextColumn(
@@ -820,4 +901,4 @@ class TextChapterLayout(
return code == 8203 || code == 8204 || code == 8205 || code == 8288
}
}
}