[新增] 正文支持HTML渲染(@Luoyacheng),标题支持正则分割 #598
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -67,7 +67,7 @@ val appDb by lazy {
|
||||
}
|
||||
|
||||
@Database(
|
||||
version = 81,
|
||||
version = 82,
|
||||
exportSchema = true,
|
||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||
@@ -114,7 +114,8 @@ val appDb by lazy {
|
||||
AutoMigration(from = 77, to = 78),
|
||||
AutoMigration(from = 78, to = 79),
|
||||
AutoMigration(from = 79, to = 80),
|
||||
AutoMigration(from = 80, to = 81)
|
||||
AutoMigration(from = 80, to = 81),
|
||||
AutoMigration(from = 81, to = 82)
|
||||
]
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
@@ -36,7 +36,7 @@ data class BookSource(
|
||||
var bookSourceName: String = "",
|
||||
// 分组
|
||||
var bookSourceGroup: String? = null,
|
||||
// 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站)
|
||||
// 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站), 4 视频
|
||||
@BookSourceType.Type
|
||||
var bookSourceType: Int = 0,
|
||||
// 详情页url正则
|
||||
@@ -94,7 +94,11 @@ data class BookSource(
|
||||
// 正文页规则
|
||||
var ruleContent: ContentRule? = null,
|
||||
// 段评规则
|
||||
var ruleReview: ReviewRule? = null
|
||||
var ruleReview: ReviewRule? = null,
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
var eventListener: Boolean = false, // 是否监听事件来执行回调规则
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
var customButton: Boolean = false //由书源控制的自定义按钮
|
||||
) : Parcelable, BaseSource {
|
||||
|
||||
override fun getTag(): String {
|
||||
@@ -207,7 +211,10 @@ data class BookSource(
|
||||
|
||||
fun getCheckKeyword(default: String): String {
|
||||
ruleSearch?.checkKeyWord?.let {
|
||||
if (it.isNotBlank()) {
|
||||
if (it.isNotBlank() && !it.contains("http") && !it.contains("::") && !it.contains("++") && !it.contains(
|
||||
"--"
|
||||
)
|
||||
) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import kotlinx.parcelize.Parcelize
|
||||
@Parcelize
|
||||
data class ContentRule(
|
||||
var content: String? = null,
|
||||
var subContent: String? = null, //副文规则,拼接在正文后面或者获取歌词等
|
||||
var title: String? = null, //有些网站只能在正文中获取标题
|
||||
var nextContentUrl: String? = null,
|
||||
var webJs: String? = null,
|
||||
@@ -19,6 +20,8 @@ data class ContentRule(
|
||||
var imageStyle: String? = null, //默认大小居中,FULL最大宽度
|
||||
var imageDecode: String? = null, //图片bytes二次解密js, 返回解密后的bytes
|
||||
var payAction: String? = null, //购买操作,js或者包含{{js}}的url
|
||||
/** 监听到事件后执行的回调js代码 **/
|
||||
var callBackJs: String? = null
|
||||
) : Parcelable {
|
||||
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ class ContentProcessor private constructor(
|
||||
1 -> mContent = ChineseUtils.t2s(mContent)
|
||||
2 -> mContent = ChineseUtils.s2t(mContent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
appCtx.toastOnUi("简繁转换出错")
|
||||
}
|
||||
}
|
||||
@@ -153,6 +153,7 @@ class ContentProcessor private constructor(
|
||||
}
|
||||
}
|
||||
if (useReplace && book.getUseReplaceRule()) {
|
||||
val replaceBook = book.toSearchBook()
|
||||
//替换
|
||||
effectiveReplaceRules = arrayListOf()
|
||||
mContent = mContent.lines().joinToString("\n") { it.trim() }
|
||||
@@ -165,7 +166,9 @@ class ContentProcessor private constructor(
|
||||
mContent.replace(
|
||||
item.regex,
|
||||
item.replacement,
|
||||
item.getValidTimeoutMillisecond()
|
||||
item.getValidTimeoutMillisecond(),
|
||||
chapter,
|
||||
replaceBook
|
||||
)
|
||||
} else {
|
||||
mContent.replace(item.pattern, item.replacement)
|
||||
@@ -185,6 +188,9 @@ class ContentProcessor private constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
useHtmlMap.forEach { (placeholder, originalContent) ->
|
||||
mContent = mContent.replace(placeholder, originalContent)
|
||||
}
|
||||
}
|
||||
if (includeTitle) {
|
||||
//重新添加标题
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package io.legado.app.model
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.script.rhino.runScriptWithContext
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.ui.login.SourceLoginJsExtensions
|
||||
import io.legado.app.utils.isTrue
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
||||
object SourceCallBack {
|
||||
const val CLICK_AUTHOR = "clickAuthor"
|
||||
const val LONG_CLICK_AUTHOR = "longClickAuthor"
|
||||
const val CLICK_BOOK_NAME = "clickBookName"
|
||||
const val LONG_CLICK_BOOK_NAME = "longClickBookName"
|
||||
const val CLICK_CUSTOM_BUTTON = "clickCustomButton"
|
||||
const val LONG_CLICK_CUSTOM_BUTTON = "longClickCustomButton"
|
||||
const val CLICK_SHARE_BOOK = "clickShareBook"
|
||||
const val CLICK_CLEAR_CACHE = "clickClearCache"
|
||||
const val CLICK_COPY_BOOK_URL = "clickCopyBookUrl"
|
||||
const val CLICK_COPY_TOC_URL = "clickCopyTocUrl"
|
||||
const val CLICK_COPY_PLAY_URL = "clickCopyPlayUrl"
|
||||
|
||||
const val ADD_BOOK_SHELF = "addBookShelf"
|
||||
const val DEL_BOOK_SHELF = "delBookShelf"
|
||||
const val SAVE_READ = "saveRead"
|
||||
const val START_READ = "startRead"
|
||||
const val END_READ = "endRead"
|
||||
const val START_SHELF_REFRESH = "startShelfRefresh"
|
||||
const val END_SHELF_REFRESH = "endShelfRefresh"
|
||||
fun callBackBtn(
|
||||
activity: AppCompatActivity,
|
||||
event: String,
|
||||
source: BookSource?,
|
||||
book: Book,
|
||||
chapter: BookChapter?,
|
||||
noCall: (() -> Unit)? = null
|
||||
) {
|
||||
if (source == null || !source.eventListener) {
|
||||
noCall?.invoke()
|
||||
return
|
||||
}
|
||||
val jsStr = source.getContentRule().callBackJs
|
||||
if (jsStr.isNullOrEmpty()) {
|
||||
noCall?.invoke()
|
||||
return
|
||||
}
|
||||
activity.lifecycleScope.launch(IO) {
|
||||
val java = SourceLoginJsExtensions(activity, source)
|
||||
runCatching {
|
||||
val result = runScriptWithContext {
|
||||
source.evalJS(jsStr) {
|
||||
put("event", event)
|
||||
put("java", java)
|
||||
put("result", null)
|
||||
put("book", book)
|
||||
put("chapter", chapter)
|
||||
}.toString()
|
||||
}
|
||||
if (!result.isTrue()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
noCall?.invoke()
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
AppLog.put(
|
||||
"${source.bookSourceName}\n书源执行回调事件${event}出错\n${it.localizedMessage}",
|
||||
it,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun callBackBook(
|
||||
event: String,
|
||||
source: BookSource?,
|
||||
book: Book?,
|
||||
chapter: BookChapter? = null
|
||||
) {
|
||||
if (source == null || book == null || !source.eventListener) return
|
||||
val jsStr = source.getContentRule().callBackJs
|
||||
if (jsStr.isNullOrEmpty()) return
|
||||
Coroutine.async {
|
||||
withTimeout(60000L) {
|
||||
runScriptWithContext(coroutineContext) {
|
||||
source.evalJS(jsStr) {
|
||||
put("event", event)
|
||||
put("result", null)
|
||||
put("book", book)
|
||||
put("chapter", chapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onError {
|
||||
AppLog.put(
|
||||
"${source.bookSourceName}\n书源执行回调事件${event}出错\n${it.localizedMessage}",
|
||||
it,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun callBackSource(scope: CoroutineScope, event: String, source: BookSource) {
|
||||
val jsStr = source.getContentRule().callBackJs
|
||||
if (jsStr.isNullOrEmpty()) return
|
||||
scope.launch(IO) {
|
||||
runCatching {
|
||||
withTimeout(30000L) {
|
||||
runScriptWithContext {
|
||||
source.evalJS(jsStr) {
|
||||
put("event", event)
|
||||
put("result", null)
|
||||
put("book", null)
|
||||
put("chapter", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
AppLog.put(
|
||||
"${source.bookSourceName}\n书源执行回调事件${event}出错\n${it.localizedMessage}",
|
||||
it,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -190,6 +190,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
viewModel.replaceRuleChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private val searchContentActivity =
|
||||
registerForActivityResult(StartActivityContract(SearchContentActivity::class.java)) {
|
||||
val data = it.data ?: return@registerForActivityResult
|
||||
@@ -1409,8 +1410,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
override fun showLogin() {
|
||||
ReadBook.bookSource?.let {
|
||||
startActivity<SourceLoginActivity> {
|
||||
putExtra("type", "bookSource")
|
||||
putExtra("key", it.bookSourceUrl)
|
||||
putExtra("bookType", BookType.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config
|
||||
}
|
||||
|
||||
binding.btnTitleSegType.setOnClickListener {
|
||||
val types = arrayOf("不分段", "按字符数分段", "按标志字符串分段")
|
||||
val types = arrayOf("不分段", "按字符数分段", "按标志字符串分段", "正则表达式分段")
|
||||
val current = ReadBookConfig.titleSegType
|
||||
|
||||
alert(title = "选择标题分段模式") {
|
||||
|
||||
@@ -20,6 +20,7 @@ import io.legado.app.ui.book.read.page.entities.column.BaseColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.ButtonColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.ImageColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.ReviewColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextBaseColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextHtmlColumn
|
||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||
|
||||
@@ -33,7 +33,7 @@ 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.TextPos
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextBaseColumn
|
||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
|
||||
import io.legado.app.ui.book.read.page.provider.TextPageFactory
|
||||
@@ -371,7 +371,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
|
||||
return@run
|
||||
}
|
||||
val column = textLine.getColumn(j)
|
||||
if (column is TextColumn) {
|
||||
if (column is TextBaseColumn) {
|
||||
ci += column.charData.length
|
||||
} else {
|
||||
ci++
|
||||
|
||||
@@ -12,8 +12,8 @@ import io.legado.app.help.PaintPool
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.ui.book.read.page.ContentTextView
|
||||
import io.legado.app.ui.book.read.page.TextBaseColumn
|
||||
import io.legado.app.ui.book.read.page.entities.TextChapter.Companion.emptyTextChapter
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextBaseColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextColumn
|
||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||
import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
|
||||
|
||||
+1
-3
@@ -1,6 +1,4 @@
|
||||
package io.legado.app.ui.book.read.page
|
||||
|
||||
import io.legado.app.ui.book.read.page.entities.column.BaseColumn
|
||||
package io.legado.app.ui.book.read.page.entities.column
|
||||
|
||||
/**
|
||||
* 文字基列
|
||||
@@ -16,19 +16,19 @@ import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||
data class TextColumn(
|
||||
override var start: Float,
|
||||
override var end: Float,
|
||||
val charData: String,
|
||||
) : BaseColumn {
|
||||
override val charData: String,
|
||||
) : TextBaseColumn {
|
||||
|
||||
override var textLine: TextLine = emptyTextLine
|
||||
|
||||
var selected: Boolean = false
|
||||
override var selected: Boolean = false
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
textLine.invalidate()
|
||||
}
|
||||
field = value
|
||||
}
|
||||
var isSearchResult: Boolean = false
|
||||
override var isSearchResult: Boolean = false
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
textLine.invalidate()
|
||||
@@ -47,15 +47,11 @@ data class TextColumn(
|
||||
} else {
|
||||
ChapterProvider.contentPaint
|
||||
}
|
||||
val textColor = if (!textLine.useUnderline && (textLine.isReadAloud || isSearchResult)) {
|
||||
val textColor = if (textLine.isReadAloud || isSearchResult) {
|
||||
ReadBookConfig.textAccentColor
|
||||
} else {
|
||||
ReadBookConfig.textColor
|
||||
}
|
||||
val originalSize = textPaint.textSize
|
||||
textLine.titleTextSize?.let {
|
||||
textPaint.textSize = it
|
||||
}
|
||||
if (textPaint.color != textColor) {
|
||||
textPaint.color = textColor
|
||||
}
|
||||
@@ -70,7 +66,6 @@ data class TextColumn(
|
||||
if (selected) {
|
||||
canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
|
||||
}
|
||||
textPaint.textSize = originalSize
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import android.text.TextPaint
|
||||
import androidx.annotation.Keep
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.ui.book.read.page.ContentTextView
|
||||
import io.legado.app.ui.book.read.page.TextBaseColumn
|
||||
import io.legado.app.ui.book.read.page.entities.TextLine
|
||||
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
|
||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||
|
||||
/**
|
||||
@@ -19,11 +19,11 @@ data class TextHtmlColumn(
|
||||
override var end: Float,
|
||||
override val charData: String,
|
||||
val mTextSize: Float,
|
||||
val mTextColor: Int,
|
||||
val mTextColor: Int?,
|
||||
val linkUrl: String?
|
||||
) : TextBaseColumn {
|
||||
|
||||
override var textLine: TextLine = TextLine.Companion.emptyTextLine
|
||||
override var textLine: TextLine = emptyTextLine
|
||||
|
||||
private val textPaint: TextPaint by lazy {
|
||||
TextPaint(ChapterProvider.contentPaint).apply {
|
||||
@@ -66,7 +66,7 @@ data class TextHtmlColumn(
|
||||
color = if (textLine.isReadAloud || isSearchResult) {
|
||||
ReadBookConfig.textAccentColor
|
||||
} else {
|
||||
mTextColor
|
||||
mTextColor ?: ReadBookConfig.textColor
|
||||
}
|
||||
isUnderlineText = false
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ import android.text.Spanned
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.ImageSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.util.Size
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.util.component1
|
||||
import androidx.core.util.component2
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.constant.AppPattern.noWordCountRegex
|
||||
@@ -31,7 +34,9 @@ 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.column.BaseColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.ImageColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextBaseColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextColumn
|
||||
import io.legado.app.ui.book.read.page.entities.column.TextHtmlColumn
|
||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider.reviewChar
|
||||
@@ -308,7 +313,7 @@ class TextChapterLayout(
|
||||
prepareNextPageIfNeed()
|
||||
return@forEach
|
||||
} else if (text.startsWith("<usehtml>")) {
|
||||
setTypeHtml(book, text.substring(9, text.lastIndexOf("<")))
|
||||
setTypeHtml(imageStyle, book, text.substring(9, text.lastIndexOf("<")))
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
@@ -553,6 +558,7 @@ class TextChapterLayout(
|
||||
* 排版html样式
|
||||
*/
|
||||
private suspend fun setTypeHtml(
|
||||
imageStyle: String?,
|
||||
book: Book,
|
||||
htmlContent: String,
|
||||
) {
|
||||
@@ -589,8 +595,7 @@ class TextChapterLayout(
|
||||
continue
|
||||
}
|
||||
val textLine = TextLine(isHtml = true)
|
||||
val lineText = spanned.subSequence(lineStart, lineEnd).toString()
|
||||
textLine.text = lineText //文本
|
||||
val lineText = StringBuilder()
|
||||
val lineLeft = staticLayout.getLineLeft(lineIndex)
|
||||
textLine.startX = absStartX + lineLeft //x坐标
|
||||
val mLineTop = staticLayout.getLineTop(lineIndex).toFloat()
|
||||
@@ -599,45 +604,121 @@ class TextChapterLayout(
|
||||
prepareNextPageIfNeed(durY + lineHeight)
|
||||
textLine.upTopBottom(durY, lineHeight, textPaint.fontMetrics) //y坐标
|
||||
|
||||
val columns = mutableListOf<TextHtmlColumn>()
|
||||
val columns = mutableListOf<BaseColumn>()
|
||||
var charIndex = lineStart
|
||||
while (charIndex < lineEnd) {
|
||||
val char = spanned[charIndex].toString()
|
||||
lineText.append(char)
|
||||
if (char == "\n") {
|
||||
textLine.isParagraphEnd = true
|
||||
durY += lineHeight * paragraphSpacing / 10f //段距
|
||||
charIndex++
|
||||
continue
|
||||
}
|
||||
val charX = staticLayout.getPrimaryHorizontal(charIndex) + lineLeft
|
||||
val charX = staticLayout.getPrimaryHorizontal(charIndex)
|
||||
val textSize = extractTextSize(spanned, charIndex, textPaint.textSize)
|
||||
val textColor = extractTextColor(spanned, charIndex, textPaint.color)
|
||||
val textColor = extractTextColor(spanned, charIndex)
|
||||
val linkUrl = extractLinkUrl(spanned, charIndex)
|
||||
|
||||
val charRight = if (charIndex + 1 < lineEnd) {
|
||||
staticLayout.getPrimaryHorizontal(charIndex + 1) + lineLeft
|
||||
staticLayout.getPrimaryHorizontal(charIndex + 1)
|
||||
} else {
|
||||
tempPaint.textSize = textSize
|
||||
val charWidth = tempPaint.measureText(char)
|
||||
charX + charWidth
|
||||
}
|
||||
var addedImage = false
|
||||
spanned.getSpans(charIndex, charIndex + 1, ImageSpan::class.java).firstOrNull()
|
||||
?.let { span -> //处理图片
|
||||
val source = span.source ?: return@let
|
||||
val urlMatcher = paramPattern.matcher(source)
|
||||
if (urlMatcher.find()) {
|
||||
val urlOptionStr = source.substring(urlMatcher.end())
|
||||
val style =
|
||||
GSON.fromJsonObject<Map<String, String>>(urlOptionStr).getOrNull()
|
||||
?: return@let
|
||||
var iStyle = style["style"]
|
||||
val width = style["width"]
|
||||
val click = style["click"]
|
||||
var imgSize =
|
||||
ImageProvider.getImageSize(book, source, ReadBook.bookSource)
|
||||
width?.let {
|
||||
if (width.endsWith("%")) {
|
||||
width.dropLast(1).toIntOrNull()?.let { percentage ->
|
||||
val imgWidth = visibleWidth * percentage / 100
|
||||
val (sizeHeight, sizeWidth) = imgSize
|
||||
imgSize = Size(imgWidth, sizeHeight * imgWidth / sizeWidth)
|
||||
}
|
||||
} else {
|
||||
width.toIntOrNull()?.let { width ->
|
||||
val (sizeHeight, sizeWidth) = imgSize
|
||||
imgSize = Size(width, sizeHeight * width / sizeWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (iStyle == null) {
|
||||
iStyle = if (imgSize.width < 80 && imgSize.height < 80) {
|
||||
"text"
|
||||
} else {
|
||||
imageStyle
|
||||
}
|
||||
}
|
||||
when (iStyle?.uppercase()) {
|
||||
"TEXT" -> {
|
||||
ImageProvider.cacheImage(book, source, ReadBook.bookSource)
|
||||
columns.add(
|
||||
ImageColumn(
|
||||
start = absStartX + charX,
|
||||
end = absStartX + charRight,
|
||||
src = source,
|
||||
click = click
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
columns.add(
|
||||
TextHtmlColumn(
|
||||
absStartX + charX,
|
||||
absStartX + charRight,
|
||||
char,
|
||||
textSize,
|
||||
textColor,
|
||||
linkUrl
|
||||
else -> {
|
||||
setTypeImage(
|
||||
book,
|
||||
source,
|
||||
contentPaintTextHeight,
|
||||
iStyle,
|
||||
imgSize,
|
||||
click
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val imgSize =
|
||||
ImageProvider.getImageSize(book, source, ReadBook.bookSource)
|
||||
setTypeImage(
|
||||
book,
|
||||
source,
|
||||
contentPaintTextHeight,
|
||||
imageStyle,
|
||||
imgSize,
|
||||
null
|
||||
)
|
||||
}
|
||||
addedImage = true
|
||||
}
|
||||
if (!addedImage) {
|
||||
columns.add(
|
||||
TextHtmlColumn(
|
||||
absStartX + charX,
|
||||
absStartX + charRight,
|
||||
char,
|
||||
textSize,
|
||||
textColor,
|
||||
linkUrl
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
charIndex++
|
||||
if (charIndex == lineEnd && lineIndex == staticLayout.lineCount - 1) {
|
||||
textLine.isParagraphEnd = true
|
||||
durY += lineHeight * paragraphSpacing / 10f //段距
|
||||
}
|
||||
}
|
||||
textLine.text = lineText.toString()
|
||||
if (textFullJustify && !textLine.isParagraphEnd) {
|
||||
justifyHtmlLine(columns, textLine, visibleWidth)
|
||||
} else {
|
||||
@@ -658,7 +739,7 @@ class TextChapterLayout(
|
||||
* 对HTML行进行两端对齐
|
||||
*/
|
||||
private fun justifyHtmlLine(
|
||||
columns: MutableList<TextHtmlColumn>,
|
||||
columns: MutableList<BaseColumn>,
|
||||
textLine: TextLine,
|
||||
lineWidth: Int
|
||||
) {
|
||||
@@ -676,7 +757,9 @@ class TextChapterLayout(
|
||||
}
|
||||
|
||||
// 统计空格数量
|
||||
val spaceCount = columns.count { it.charData == " " }
|
||||
val spaceCount = columns.count {
|
||||
(it as? TextBaseColumn)?.charData == " "
|
||||
}
|
||||
|
||||
if (spaceCount > 1) {
|
||||
// 多个空格:调整单词间距
|
||||
@@ -689,7 +772,7 @@ class TextChapterLayout(
|
||||
val col = columns[i]
|
||||
val width = col.end - col.start
|
||||
|
||||
if (col.charData == " " && i != columns.lastIndex) {
|
||||
if ((col as? TextBaseColumn)?.charData == " " && i != columns.lastIndex) {
|
||||
// 空格,增加额外的间距
|
||||
col.start = currentX
|
||||
col.end = currentX + width + spaceIncrement
|
||||
@@ -746,23 +829,9 @@ class TextChapterLayout(
|
||||
return defaultSize
|
||||
}
|
||||
|
||||
private fun extractTextColor(spanned: Spanned, index: Int, defaultColor: Int): Int {
|
||||
// 检查 ForegroundColorSpan(前景色)
|
||||
private fun extractTextColor(spanned: Spanned, index: Int): Int? {
|
||||
val foregroundSpans = spanned.getSpans(index, index + 1, ForegroundColorSpan::class.java)
|
||||
foregroundSpans.firstOrNull()?.let { span ->
|
||||
return span.foregroundColor
|
||||
}
|
||||
|
||||
// 2. 检查自定义的彩色 Span
|
||||
// val customColorSpans = spanned.getSpans(index, index + 1, CharacterStyle::class.java)
|
||||
// customColorSpans.firstOrNull()?.let { span ->
|
||||
// if (span is ForegroundColorSpan) {
|
||||
// return span.foregroundColor
|
||||
// }
|
||||
// }
|
||||
|
||||
// 默认返回 Paint 的颜色
|
||||
return defaultColor
|
||||
return foregroundSpans.firstOrNull()?.foregroundColor
|
||||
}
|
||||
|
||||
private fun extractLinkUrl(spanned: Spanned, index: Int): String? {
|
||||
|
||||
@@ -3,6 +3,9 @@ package io.legado.app.utils
|
||||
import androidx.core.os.postDelayed
|
||||
import com.script.ScriptBindings
|
||||
import com.script.rhino.RhinoScriptEngine
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.exception.RegexTimeoutException
|
||||
import io.legado.app.help.CrashHandler
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
@@ -10,7 +13,6 @@ import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import splitties.init.appCtx
|
||||
import java.util.regex.Matcher
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
@@ -19,10 +21,23 @@ private val handler by lazy { buildMainHandler() }
|
||||
/**
|
||||
* 带有超时检测的正则替换
|
||||
*/
|
||||
fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String {
|
||||
fun CharSequence.replace(
|
||||
regex: Regex,
|
||||
replacement: String,
|
||||
timeout: Long,
|
||||
chapter: BookChapter? = null,
|
||||
book: SearchBook? = null
|
||||
): String {
|
||||
val charSequence = this@replace
|
||||
val isJs = replacement.startsWith("@js:")
|
||||
val replacement1 = if (isJs) replacement.substring(4) else replacement
|
||||
val book = if (isJs) {
|
||||
book ?: chapter?.bookUrl?.let {
|
||||
appDb.searchBookDao.getSearchBook(it) ?: appDb.bookDao.getBook(it)?.toSearchBook()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return runBlocking {
|
||||
suspendCancellableCoroutine { block ->
|
||||
val coroutine = Coroutine.async(executeContext = IO) {
|
||||
@@ -35,9 +50,11 @@ fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): Stri
|
||||
val jsResult = RhinoScriptEngine.run {
|
||||
val bindings = ScriptBindings()
|
||||
bindings["result"] = matcher.group()
|
||||
bindings["chapter"] = chapter
|
||||
bindings["book"] = book
|
||||
eval(replacement1, bindings)
|
||||
}.toString()
|
||||
val quotedResult = Matcher.quoteReplacement(jsResult)
|
||||
val quotedResult = jsResult.quoteReplacementJs()
|
||||
matcher.appendReplacement(stringBuffer, quotedResult)
|
||||
} else {
|
||||
matcher.appendReplacement(stringBuffer, replacement1)
|
||||
@@ -66,5 +83,4 @@ fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): Stri
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user