[新增] 正文支持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(
|
@Database(
|
||||||
version = 81,
|
version = 82,
|
||||||
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,
|
||||||
@@ -114,7 +114,8 @@ val appDb by lazy {
|
|||||||
AutoMigration(from = 77, to = 78),
|
AutoMigration(from = 77, to = 78),
|
||||||
AutoMigration(from = 78, to = 79),
|
AutoMigration(from = 78, to = 79),
|
||||||
AutoMigration(from = 79, to = 80),
|
AutoMigration(from = 79, to = 80),
|
||||||
AutoMigration(from = 80, to = 81)
|
AutoMigration(from = 80, to = 81),
|
||||||
|
AutoMigration(from = 81, to = 82)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ data class BookSource(
|
|||||||
var bookSourceName: String = "",
|
var bookSourceName: String = "",
|
||||||
// 分组
|
// 分组
|
||||||
var bookSourceGroup: String? = null,
|
var bookSourceGroup: String? = null,
|
||||||
// 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站)
|
// 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站), 4 视频
|
||||||
@BookSourceType.Type
|
@BookSourceType.Type
|
||||||
var bookSourceType: Int = 0,
|
var bookSourceType: Int = 0,
|
||||||
// 详情页url正则
|
// 详情页url正则
|
||||||
@@ -94,7 +94,11 @@ data class BookSource(
|
|||||||
// 正文页规则
|
// 正文页规则
|
||||||
var ruleContent: ContentRule? = null,
|
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 {
|
) : Parcelable, BaseSource {
|
||||||
|
|
||||||
override fun getTag(): String {
|
override fun getTag(): String {
|
||||||
@@ -207,7 +211,10 @@ data class BookSource(
|
|||||||
|
|
||||||
fun getCheckKeyword(default: String): String {
|
fun getCheckKeyword(default: String): String {
|
||||||
ruleSearch?.checkKeyWord?.let {
|
ruleSearch?.checkKeyWord?.let {
|
||||||
if (it.isNotBlank()) {
|
if (it.isNotBlank() && !it.contains("http") && !it.contains("::") && !it.contains("++") && !it.contains(
|
||||||
|
"--"
|
||||||
|
)
|
||||||
|
) {
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import kotlinx.parcelize.Parcelize
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
data class ContentRule(
|
data class ContentRule(
|
||||||
var content: String? = null,
|
var content: String? = null,
|
||||||
|
var subContent: String? = null, //副文规则,拼接在正文后面或者获取歌词等
|
||||||
var title: String? = null, //有些网站只能在正文中获取标题
|
var title: String? = null, //有些网站只能在正文中获取标题
|
||||||
var nextContentUrl: String? = null,
|
var nextContentUrl: String? = null,
|
||||||
var webJs: String? = null,
|
var webJs: String? = null,
|
||||||
@@ -19,6 +20,8 @@ data class ContentRule(
|
|||||||
var imageStyle: String? = null, //默认大小居中,FULL最大宽度
|
var imageStyle: String? = null, //默认大小居中,FULL最大宽度
|
||||||
var imageDecode: String? = null, //图片bytes二次解密js, 返回解密后的bytes
|
var imageDecode: String? = null, //图片bytes二次解密js, 返回解密后的bytes
|
||||||
var payAction: String? = null, //购买操作,js或者包含{{js}}的url
|
var payAction: String? = null, //购买操作,js或者包含{{js}}的url
|
||||||
|
/** 监听到事件后执行的回调js代码 **/
|
||||||
|
var callBackJs: String? = null
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class ContentProcessor private constructor(
|
|||||||
1 -> mContent = ChineseUtils.t2s(mContent)
|
1 -> mContent = ChineseUtils.t2s(mContent)
|
||||||
2 -> mContent = ChineseUtils.s2t(mContent)
|
2 -> mContent = ChineseUtils.s2t(mContent)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
appCtx.toastOnUi("简繁转换出错")
|
appCtx.toastOnUi("简繁转换出错")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +153,7 @@ class ContentProcessor private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (useReplace && book.getUseReplaceRule()) {
|
if (useReplace && book.getUseReplaceRule()) {
|
||||||
|
val replaceBook = book.toSearchBook()
|
||||||
//替换
|
//替换
|
||||||
effectiveReplaceRules = arrayListOf()
|
effectiveReplaceRules = arrayListOf()
|
||||||
mContent = mContent.lines().joinToString("\n") { it.trim() }
|
mContent = mContent.lines().joinToString("\n") { it.trim() }
|
||||||
@@ -165,7 +166,9 @@ class ContentProcessor private constructor(
|
|||||||
mContent.replace(
|
mContent.replace(
|
||||||
item.regex,
|
item.regex,
|
||||||
item.replacement,
|
item.replacement,
|
||||||
item.getValidTimeoutMillisecond()
|
item.getValidTimeoutMillisecond(),
|
||||||
|
chapter,
|
||||||
|
replaceBook
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
mContent.replace(item.pattern, item.replacement)
|
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) {
|
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()
|
viewModel.replaceRuleChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val searchContentActivity =
|
private val searchContentActivity =
|
||||||
registerForActivityResult(StartActivityContract(SearchContentActivity::class.java)) {
|
registerForActivityResult(StartActivityContract(SearchContentActivity::class.java)) {
|
||||||
val data = it.data ?: return@registerForActivityResult
|
val data = it.data ?: return@registerForActivityResult
|
||||||
@@ -1409,8 +1410,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
|||||||
override fun showLogin() {
|
override fun showLogin() {
|
||||||
ReadBook.bookSource?.let {
|
ReadBook.bookSource?.let {
|
||||||
startActivity<SourceLoginActivity> {
|
startActivity<SourceLoginActivity> {
|
||||||
putExtra("type", "bookSource")
|
putExtra("bookType", BookType.text)
|
||||||
putExtra("key", it.bookSourceUrl)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.btnTitleSegType.setOnClickListener {
|
binding.btnTitleSegType.setOnClickListener {
|
||||||
val types = arrayOf("不分段", "按字符数分段", "按标志字符串分段")
|
val types = arrayOf("不分段", "按字符数分段", "按标志字符串分段", "正则表达式分段")
|
||||||
val current = ReadBookConfig.titleSegType
|
val current = ReadBookConfig.titleSegType
|
||||||
|
|
||||||
alert(title = "选择标题分段模式") {
|
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.ButtonColumn
|
||||||
import io.legado.app.ui.book.read.page.entities.column.ImageColumn
|
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.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.TextColumn
|
||||||
import io.legado.app.ui.book.read.page.entities.column.TextHtmlColumn
|
import io.legado.app.ui.book.read.page.entities.column.TextHtmlColumn
|
||||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
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.TextLine
|
||||||
import io.legado.app.ui.book.read.page.entities.TextPage
|
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.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.ChapterProvider
|
||||||
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
|
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
|
||||||
import io.legado.app.ui.book.read.page.provider.TextPageFactory
|
import io.legado.app.ui.book.read.page.provider.TextPageFactory
|
||||||
@@ -371,7 +371,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
|
|||||||
return@run
|
return@run
|
||||||
}
|
}
|
||||||
val column = textLine.getColumn(j)
|
val column = textLine.getColumn(j)
|
||||||
if (column is TextColumn) {
|
if (column is TextBaseColumn) {
|
||||||
ci += column.charData.length
|
ci += column.charData.length
|
||||||
} else {
|
} else {
|
||||||
ci++
|
ci++
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import io.legado.app.help.PaintPool
|
|||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.ui.book.read.page.ContentTextView
|
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.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.entities.column.TextColumn
|
||||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||||
import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
|
import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
|
||||||
|
|||||||
+1
-3
@@ -1,6 +1,4 @@
|
|||||||
package io.legado.app.ui.book.read.page
|
package io.legado.app.ui.book.read.page.entities.column
|
||||||
|
|
||||||
import io.legado.app.ui.book.read.page.entities.column.BaseColumn
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文字基列
|
* 文字基列
|
||||||
@@ -16,19 +16,19 @@ import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
|||||||
data class TextColumn(
|
data class TextColumn(
|
||||||
override var start: Float,
|
override var start: Float,
|
||||||
override var end: Float,
|
override var end: Float,
|
||||||
val charData: String,
|
override val charData: String,
|
||||||
) : BaseColumn {
|
) : TextBaseColumn {
|
||||||
|
|
||||||
override var textLine: TextLine = emptyTextLine
|
override var textLine: TextLine = emptyTextLine
|
||||||
|
|
||||||
var selected: Boolean = false
|
override var selected: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field != value) {
|
if (field != value) {
|
||||||
textLine.invalidate()
|
textLine.invalidate()
|
||||||
}
|
}
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
var isSearchResult: Boolean = false
|
override var isSearchResult: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field != value) {
|
if (field != value) {
|
||||||
textLine.invalidate()
|
textLine.invalidate()
|
||||||
@@ -47,15 +47,11 @@ data class TextColumn(
|
|||||||
} else {
|
} else {
|
||||||
ChapterProvider.contentPaint
|
ChapterProvider.contentPaint
|
||||||
}
|
}
|
||||||
val textColor = if (!textLine.useUnderline && (textLine.isReadAloud || isSearchResult)) {
|
val textColor = if (textLine.isReadAloud || isSearchResult) {
|
||||||
ReadBookConfig.textAccentColor
|
ReadBookConfig.textAccentColor
|
||||||
} else {
|
} else {
|
||||||
ReadBookConfig.textColor
|
ReadBookConfig.textColor
|
||||||
}
|
}
|
||||||
val originalSize = textPaint.textSize
|
|
||||||
textLine.titleTextSize?.let {
|
|
||||||
textPaint.textSize = it
|
|
||||||
}
|
|
||||||
if (textPaint.color != textColor) {
|
if (textPaint.color != textColor) {
|
||||||
textPaint.color = textColor
|
textPaint.color = textColor
|
||||||
}
|
}
|
||||||
@@ -70,7 +66,6 @@ data class TextColumn(
|
|||||||
if (selected) {
|
if (selected) {
|
||||||
canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
|
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 androidx.annotation.Keep
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.ui.book.read.page.ContentTextView
|
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
|
||||||
|
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
|
||||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,11 +19,11 @@ data class TextHtmlColumn(
|
|||||||
override var end: Float,
|
override var end: Float,
|
||||||
override val charData: String,
|
override val charData: String,
|
||||||
val mTextSize: Float,
|
val mTextSize: Float,
|
||||||
val mTextColor: Int,
|
val mTextColor: Int?,
|
||||||
val linkUrl: String?
|
val linkUrl: String?
|
||||||
) : TextBaseColumn {
|
) : TextBaseColumn {
|
||||||
|
|
||||||
override var textLine: TextLine = TextLine.Companion.emptyTextLine
|
override var textLine: TextLine = emptyTextLine
|
||||||
|
|
||||||
private val textPaint: TextPaint by lazy {
|
private val textPaint: TextPaint by lazy {
|
||||||
TextPaint(ChapterProvider.contentPaint).apply {
|
TextPaint(ChapterProvider.contentPaint).apply {
|
||||||
@@ -66,7 +66,7 @@ data class TextHtmlColumn(
|
|||||||
color = if (textLine.isReadAloud || isSearchResult) {
|
color = if (textLine.isReadAloud || isSearchResult) {
|
||||||
ReadBookConfig.textAccentColor
|
ReadBookConfig.textAccentColor
|
||||||
} else {
|
} else {
|
||||||
mTextColor
|
mTextColor ?: ReadBookConfig.textColor
|
||||||
}
|
}
|
||||||
isUnderlineText = false
|
isUnderlineText = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import android.text.Spanned
|
|||||||
import android.text.StaticLayout
|
import android.text.StaticLayout
|
||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.ImageSpan
|
||||||
import android.text.style.RelativeSizeSpan
|
import android.text.style.RelativeSizeSpan
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.core.text.parseAsHtml
|
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.AppLog
|
||||||
import io.legado.app.constant.AppPattern
|
import io.legado.app.constant.AppPattern
|
||||||
import io.legado.app.constant.AppPattern.noWordCountRegex
|
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.TextChapter
|
||||||
import io.legado.app.ui.book.read.page.entities.TextLine
|
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.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.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.TextColumn
|
||||||
import io.legado.app.ui.book.read.page.entities.column.TextHtmlColumn
|
import io.legado.app.ui.book.read.page.entities.column.TextHtmlColumn
|
||||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider.reviewChar
|
import io.legado.app.ui.book.read.page.provider.ChapterProvider.reviewChar
|
||||||
@@ -308,7 +313,7 @@ class TextChapterLayout(
|
|||||||
prepareNextPageIfNeed()
|
prepareNextPageIfNeed()
|
||||||
return@forEach
|
return@forEach
|
||||||
} else if (text.startsWith("<usehtml>")) {
|
} else if (text.startsWith("<usehtml>")) {
|
||||||
setTypeHtml(book, text.substring(9, text.lastIndexOf("<")))
|
setTypeHtml(imageStyle, book, text.substring(9, text.lastIndexOf("<")))
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -553,6 +558,7 @@ class TextChapterLayout(
|
|||||||
* 排版html样式
|
* 排版html样式
|
||||||
*/
|
*/
|
||||||
private suspend fun setTypeHtml(
|
private suspend fun setTypeHtml(
|
||||||
|
imageStyle: String?,
|
||||||
book: Book,
|
book: Book,
|
||||||
htmlContent: String,
|
htmlContent: String,
|
||||||
) {
|
) {
|
||||||
@@ -589,8 +595,7 @@ class TextChapterLayout(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val textLine = TextLine(isHtml = true)
|
val textLine = TextLine(isHtml = true)
|
||||||
val lineText = spanned.subSequence(lineStart, lineEnd).toString()
|
val lineText = StringBuilder()
|
||||||
textLine.text = lineText //文本
|
|
||||||
val lineLeft = staticLayout.getLineLeft(lineIndex)
|
val lineLeft = staticLayout.getLineLeft(lineIndex)
|
||||||
textLine.startX = absStartX + lineLeft //x坐标
|
textLine.startX = absStartX + lineLeft //x坐标
|
||||||
val mLineTop = staticLayout.getLineTop(lineIndex).toFloat()
|
val mLineTop = staticLayout.getLineTop(lineIndex).toFloat()
|
||||||
@@ -599,45 +604,121 @@ class TextChapterLayout(
|
|||||||
prepareNextPageIfNeed(durY + lineHeight)
|
prepareNextPageIfNeed(durY + lineHeight)
|
||||||
textLine.upTopBottom(durY, lineHeight, textPaint.fontMetrics) //y坐标
|
textLine.upTopBottom(durY, lineHeight, textPaint.fontMetrics) //y坐标
|
||||||
|
|
||||||
val columns = mutableListOf<TextHtmlColumn>()
|
val columns = mutableListOf<BaseColumn>()
|
||||||
var charIndex = lineStart
|
var charIndex = lineStart
|
||||||
while (charIndex < lineEnd) {
|
while (charIndex < lineEnd) {
|
||||||
val char = spanned[charIndex].toString()
|
val char = spanned[charIndex].toString()
|
||||||
|
lineText.append(char)
|
||||||
if (char == "\n") {
|
if (char == "\n") {
|
||||||
textLine.isParagraphEnd = true
|
textLine.isParagraphEnd = true
|
||||||
durY += lineHeight * paragraphSpacing / 10f //段距
|
durY += lineHeight * paragraphSpacing / 10f //段距
|
||||||
charIndex++
|
charIndex++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val charX = staticLayout.getPrimaryHorizontal(charIndex) + lineLeft
|
val charX = staticLayout.getPrimaryHorizontal(charIndex)
|
||||||
val textSize = extractTextSize(spanned, charIndex, textPaint.textSize)
|
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 linkUrl = extractLinkUrl(spanned, charIndex)
|
||||||
|
|
||||||
val charRight = if (charIndex + 1 < lineEnd) {
|
val charRight = if (charIndex + 1 < lineEnd) {
|
||||||
staticLayout.getPrimaryHorizontal(charIndex + 1) + lineLeft
|
staticLayout.getPrimaryHorizontal(charIndex + 1)
|
||||||
} else {
|
} else {
|
||||||
tempPaint.textSize = textSize
|
tempPaint.textSize = textSize
|
||||||
val charWidth = tempPaint.measureText(char)
|
val charWidth = tempPaint.measureText(char)
|
||||||
charX + charWidth
|
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(
|
else -> {
|
||||||
TextHtmlColumn(
|
setTypeImage(
|
||||||
absStartX + charX,
|
book,
|
||||||
absStartX + charRight,
|
source,
|
||||||
char,
|
contentPaintTextHeight,
|
||||||
textSize,
|
iStyle,
|
||||||
textColor,
|
imgSize,
|
||||||
linkUrl
|
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++
|
charIndex++
|
||||||
if (charIndex == lineEnd && lineIndex == staticLayout.lineCount - 1) {
|
if (charIndex == lineEnd && lineIndex == staticLayout.lineCount - 1) {
|
||||||
textLine.isParagraphEnd = true
|
textLine.isParagraphEnd = true
|
||||||
durY += lineHeight * paragraphSpacing / 10f //段距
|
durY += lineHeight * paragraphSpacing / 10f //段距
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
textLine.text = lineText.toString()
|
||||||
if (textFullJustify && !textLine.isParagraphEnd) {
|
if (textFullJustify && !textLine.isParagraphEnd) {
|
||||||
justifyHtmlLine(columns, textLine, visibleWidth)
|
justifyHtmlLine(columns, textLine, visibleWidth)
|
||||||
} else {
|
} else {
|
||||||
@@ -658,7 +739,7 @@ class TextChapterLayout(
|
|||||||
* 对HTML行进行两端对齐
|
* 对HTML行进行两端对齐
|
||||||
*/
|
*/
|
||||||
private fun justifyHtmlLine(
|
private fun justifyHtmlLine(
|
||||||
columns: MutableList<TextHtmlColumn>,
|
columns: MutableList<BaseColumn>,
|
||||||
textLine: TextLine,
|
textLine: TextLine,
|
||||||
lineWidth: Int
|
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) {
|
if (spaceCount > 1) {
|
||||||
// 多个空格:调整单词间距
|
// 多个空格:调整单词间距
|
||||||
@@ -689,7 +772,7 @@ class TextChapterLayout(
|
|||||||
val col = columns[i]
|
val col = columns[i]
|
||||||
val width = col.end - col.start
|
val width = col.end - col.start
|
||||||
|
|
||||||
if (col.charData == " " && i != columns.lastIndex) {
|
if ((col as? TextBaseColumn)?.charData == " " && i != columns.lastIndex) {
|
||||||
// 空格,增加额外的间距
|
// 空格,增加额外的间距
|
||||||
col.start = currentX
|
col.start = currentX
|
||||||
col.end = currentX + width + spaceIncrement
|
col.end = currentX + width + spaceIncrement
|
||||||
@@ -746,23 +829,9 @@ class TextChapterLayout(
|
|||||||
return defaultSize
|
return defaultSize
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractTextColor(spanned: Spanned, index: Int, defaultColor: Int): Int {
|
private fun extractTextColor(spanned: Spanned, index: Int): Int? {
|
||||||
// 检查 ForegroundColorSpan(前景色)
|
|
||||||
val foregroundSpans = spanned.getSpans(index, index + 1, ForegroundColorSpan::class.java)
|
val foregroundSpans = spanned.getSpans(index, index + 1, ForegroundColorSpan::class.java)
|
||||||
foregroundSpans.firstOrNull()?.let { span ->
|
return foregroundSpans.firstOrNull()?.foregroundColor
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractLinkUrl(spanned: Spanned, index: Int): String? {
|
private fun extractLinkUrl(spanned: Spanned, index: Int): String? {
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package io.legado.app.utils
|
|||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import com.script.ScriptBindings
|
import com.script.ScriptBindings
|
||||||
import com.script.rhino.RhinoScriptEngine
|
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.exception.RegexTimeoutException
|
||||||
import io.legado.app.help.CrashHandler
|
import io.legado.app.help.CrashHandler
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
@@ -10,7 +13,6 @@ import kotlinx.coroutines.Dispatchers.IO
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.util.regex.Matcher
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
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 charSequence = this@replace
|
||||||
val isJs = replacement.startsWith("@js:")
|
val isJs = replacement.startsWith("@js:")
|
||||||
val replacement1 = if (isJs) replacement.substring(4) else replacement
|
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 {
|
return runBlocking {
|
||||||
suspendCancellableCoroutine { block ->
|
suspendCancellableCoroutine { block ->
|
||||||
val coroutine = Coroutine.async(executeContext = IO) {
|
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 jsResult = RhinoScriptEngine.run {
|
||||||
val bindings = ScriptBindings()
|
val bindings = ScriptBindings()
|
||||||
bindings["result"] = matcher.group()
|
bindings["result"] = matcher.group()
|
||||||
|
bindings["chapter"] = chapter
|
||||||
|
bindings["book"] = book
|
||||||
eval(replacement1, bindings)
|
eval(replacement1, bindings)
|
||||||
}.toString()
|
}.toString()
|
||||||
val quotedResult = Matcher.quoteReplacement(jsResult)
|
val quotedResult = jsResult.quoteReplacementJs()
|
||||||
matcher.appendReplacement(stringBuffer, quotedResult)
|
matcher.appendReplacement(stringBuffer, quotedResult)
|
||||||
} else {
|
} else {
|
||||||
matcher.appendReplacement(stringBuffer, replacement1)
|
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