[新增] 正则分段标题以及更多Sigma分支功能(@Luoyacheng)

This commit is contained in:
HapeLee
2026-01-29 00:59:02 +08:00
parent b6a1814555
commit a76c25e450
12 changed files with 290 additions and 143 deletions
@@ -76,7 +76,6 @@ import java.util.zip.ZipInputStream
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* js扩展类, 在js中通过java变量调用
* 添加方法,请更新文档/legado/app/src/main/assets/help/JsHelp.md
@@ -128,7 +127,6 @@ interface JsExtensions : JsEncodeUtils {
fun ajaxAll(urlList: Array<String>): Array<StrResponse> {
return ajaxAll(urlList, false)
}
fun ajaxAll(urlList: Array<String>, skipRateLimit: Boolean): Array<StrResponse> {
return runBlocking(context) {
urlList.asFlow().mapAsync(AppConfig.threadCount) { url ->
@@ -148,7 +146,6 @@ interface JsExtensions : JsEncodeUtils {
fun ajaxTestAll(urlList: Array<String>, timeout: Int): Array<StrResponse> {
return ajaxTestAll(urlList, timeout, false)
}
fun ajaxTestAll(
urlList: Array<String>,
timeout: Int,
@@ -238,7 +235,17 @@ interface JsExtensions : JsEncodeUtils {
}
fun webViewGetSource(html: String?, url: String?, js: String?, sourceRegex: String): String? {
return webViewGetSource(html, url, js, sourceRegex, false)
return webViewGetSource(html, url, js, sourceRegex, false, 0)
}
fun webViewGetSource(
html: String?,
url: String?,
js: String?,
sourceRegex: String,
cacheFirst: Boolean
): String? {
return webViewGetSource(html, url, js, sourceRegex, cacheFirst, 0)
}
/**
@@ -249,7 +256,8 @@ interface JsExtensions : JsEncodeUtils {
url: String?,
js: String?,
sourceRegex: String,
cacheFirst: Boolean
cacheFirst: Boolean,
delayTime: Long
): String? {
if (isMainThread) {
error("webViewGetSource must be called on a background thread")
@@ -261,7 +269,8 @@ interface JsExtensions : JsEncodeUtils {
javaScript = js,
headerMap = getSource()?.getHeaderMap(true),
tag = getSource()?.getKey(),
sourceRegex = sourceRegex
sourceRegex = sourceRegex,
delayTime = delayTime
).getStrResponse().body
}
}
@@ -272,7 +281,17 @@ interface JsExtensions : JsEncodeUtils {
js: String?,
overrideUrlRegex: String
): String? {
return webViewGetOverrideUrl(html, url, js, overrideUrlRegex, false)
return webViewGetOverrideUrl(html, url, js, overrideUrlRegex, false, 0)
}
fun webViewGetOverrideUrl(
html: String?,
url: String?,
js: String?,
overrideUrlRegex: String,
cacheFirst: Boolean
): String? {
return webViewGetOverrideUrl(html, url, js, overrideUrlRegex, cacheFirst, 0)
}
/**
@@ -283,7 +302,8 @@ interface JsExtensions : JsEncodeUtils {
url: String?,
js: String?,
overrideUrlRegex: String,
cacheFirst: Boolean
cacheFirst: Boolean,
delayTime: Long
): String? {
if (isMainThread) {
error("webViewGetOverrideUrl must be called on a background thread")
@@ -295,11 +315,23 @@ interface JsExtensions : JsEncodeUtils {
javaScript = js,
headerMap = getSource()?.getHeaderMap(true),
tag = getSource()?.getKey(),
overrideUrlRegex = overrideUrlRegex
overrideUrlRegex = overrideUrlRegex,
delayTime = delayTime
).getStrResponse().body
}
}
/**
* 打开内置视频播放器
* @param url 视频播放链接
* @param title 视频的标题
* @param float 是否悬浮窗打开
*/
@JavascriptInterface
fun openVideoPlayer(url: String, title: String, float: Boolean) {
//SourceHelp.openVideoPlayer(getSource(), url, title, float)
}
/**
* 使用内置浏览器打开链接,手动验证网站防爬
* @param url 要打开的链接
@@ -1164,9 +1196,9 @@ interface JsExtensions : JsEncodeUtils {
return GSON.toJson(ReadBookConfig.durConfig)
}
/* fun getReadBookConfigMap(): Map<String, Any> {
return ReadBookConfig.durConfig.toMap()
} */
//fun getReadBookConfigMap(): Map<String, Any> {
// return ReadBookConfig.durConfig.toMap()
//}
/**
* 获取主题模式
@@ -1179,14 +1211,14 @@ interface JsExtensions : JsEncodeUtils {
/**
* 获取主题配置
*/
//@JavascriptInterface
/*fun getThemeConfig(): String {
val themeConfig = ThemeConfig.getDurConfig(appCtx)
return GSON.toJson(themeConfig)
}*/
// @JavascriptInterface
//fun getThemeConfig(): String {
// val themeConfig = ThemeConfig.getDurConfig(appCtx)
// return GSON.toJson(themeConfig)
//}
/* fun getThemeConfigMap(): Map<String, Any?> {
return ThemeConfig.getDurConfig(appCtx).toMap()
} */
//fun getThemeConfigMap(): Map<String, Any?> {
// return ThemeConfig.getDurConfig(appCtx).toMap()
//}
}
}
@@ -19,15 +19,27 @@ import io.legado.app.ui.rss.read.RssJsExtensions
import io.legado.app.utils.GSON
import io.legado.app.utils.escapeForJs
import io.legado.app.utils.fromJsonObject
import java.lang.ref.WeakReference
import java.util.UUID
@Suppress("unused")
class WebJsExtensions(
source: BaseSource,
activity: AppCompatActivity,
source: BaseSource, activity: AppCompatActivity,
private val webView: WebView,
private val bookType: Int = 0
private val bookType: Int = 0,
callback: Callback? = null
) : RssJsExtensions(activity, source) {
private val callbackRef: WeakReference<Callback> = WeakReference(callback)
interface Callback {
fun upConfig(config: String)
}
@JavascriptInterface
fun upConfig(config: String) {
callbackRef.get()?.upConfig(config)
}
private val bookAndChapter by lazy {
var book: Book? = null
var chapter: BookChapter? = null
@@ -64,35 +76,37 @@ class WebJsExtensions(
setChapter(chapter)
evalJS(jsParam[0]).toString()
}
"ajaxAwait" -> {
ajax(jsParam[0], jsParam[1].toIntOrNull()).toString()
}
"connectAwait" -> {
connect(jsParam[0], jsParam[1], jsParam[2].toIntOrNull())
}
"getAwait" -> {
get(jsParam[0], jsParam[1], jsParam[2].toIntOrNull())
}
"headAwait" -> {
head(jsParam[0], jsParam[1], jsParam[2].toIntOrNull())
}
"postAwait" -> {
post(jsParam[0], jsParam[1], jsParam[2], jsParam[3].toIntOrNull())
}
"webViewAwait" -> {
webView(jsParam[0], jsParam[1], jsParam[2], jsParam[3].toBoolean()).toString()
}
"webViewGetSourceAwait" -> {
webViewGetSource(
jsParam[0],
jsParam[1],
jsParam[2],
jsParam[3],
jsParam[4].toBoolean(),
jsParam[5].toLongOrNull() ?: 0
).toString()
}
"decryptStrAwait" -> {
createSymmetricCrypto(jsParam[0], jsParam[1], jsParam[2]).decryptStr(jsParam[3])
}
"encryptBase64Await" -> {
createSymmetricCrypto(
jsParam[0],
@@ -100,34 +114,27 @@ class WebJsExtensions(
jsParam[2]
).encryptBase64(jsParam[3])
}
"encryptHexAwait" -> {
createSymmetricCrypto(jsParam[0], jsParam[1], jsParam[2]).encryptHex(jsParam[3])
}
"createSignHexAwait" -> {
createSign(jsParam[0]).setPublicKey(jsParam[1]).setPrivateKey(jsParam[2])
.signHex(jsParam[3])
}
"downloadFileAwait" -> {
downloadFile(jsParam[0])
}
"readTxtFileAwait" -> {
readTxtFile(jsParam[0])
}
"importScriptAwait" -> {
importScript(jsParam[0])
}
"getStringAwait" -> AnalyzeRule(book, getSource()).run {
setCoroutineContext(coroutineContext)
setChapter(chapter)
getString(jsParam[0], jsParam[1])
}
else -> "error funName"
}
}.onSuccess { data ->
@@ -142,78 +149,64 @@ class WebJsExtensions(
)
}
}
@JavascriptInterface
fun toast(msg: String?) {
super.toast(msg)
}
@JavascriptInterface
fun longToast(msg: String?) {
super.longToast(msg)
}
@JavascriptInterface
fun log(msg: String?): String {
return super.log(msg).toString()
}
@JavascriptInterface
fun ajax(url: String): String? {
return super.ajax(url, 9000)
}
@JavascriptInterface
fun ajax(url: String, callTimeout: Int?): String? {
return super.ajax(url, callTimeout?.toLong())
}
@JavascriptInterface
fun connect(urlStr: String?): String {
if (urlStr.isNullOrEmpty()) return "error empty url"
return super.connect(urlStr, null, 9000).toString()
}
@JavascriptInterface
fun connect(urlStr: String, header: String): String {
return super.connect(urlStr, header, 9000).toString()
}
@JavascriptInterface
fun connect(urlStr: String, header: String, callTimeout: Int?): String {
return super.connect(urlStr, header, callTimeout?.toLong()).toString()
}
@JavascriptInterface
fun get(urlStr: String, headers: String): String {
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
return super.get(urlStr, headerMap, 9000).body()
}
@JavascriptInterface
fun get(urlStr: String, headers: String, timeout: Int?): String {
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
return super.get(urlStr, headerMap, timeout).body()
}
@JavascriptInterface
fun post(urlStr: String, body: String, headers: String): String {
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
return super.post(urlStr, body, headerMap, 9000).body()
}
@JavascriptInterface
fun post(urlStr: String, body: String, headers: String, timeout: Int?): String {
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
return super.post(urlStr, body, headerMap, timeout).body()
}
@JavascriptInterface
fun head(urlStr: String, headers: String): String {
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
return GSON.toJson(super.head(urlStr, headerMap, 9000).headers())
}
@JavascriptInterface
fun head(urlStr: String, headers: String, timeout: Int?): String {
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
@@ -222,127 +215,146 @@ class WebJsExtensions(
companion object {
private fun getRandomLetter(): Char {
val letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"
val letters = "abcdefghijklmnopqrstuvwxyz"
return letters.random()
}
val uuid by lazy { UUID.randomUUID().toString().split("-") }
val uuid2 by lazy { UUID.randomUUID().toString().split("-") }
val nameJava by lazy { getRandomLetter() + uuid[0] + uuid[1] }
val nameCache by lazy { getRandomLetter() + uuid[2] + uuid[3] }
val nameSource by lazy { getRandomLetter() + uuid[4] }
val nameBasic by lazy { getRandomLetter() + uuid2[1] + uuid2[2] }
val JSBridgeResult by lazy { getRandomLetter() + uuid2[3] + uuid2[4] }
val uuid by lazy {
UUID.randomUUID().toString().replace('-', getRandomLetter()).chunked(6)
}
val uuid2 by lazy {
UUID.randomUUID().toString().replace('-', getRandomLetter()).chunked(6)
}
val nameUrl by lazy { "https://" + uuid[0] + ".com/" + uuid2[0] + ".js" }
val nameJava by lazy { getRandomLetter() + uuid[1] + uuid2[1] }
val nameCache by lazy { getRandomLetter() + uuid[2] + uuid2[2] }
val nameSource by lazy { getRandomLetter() + uuid[3] + uuid2[3] }
val nameBasic by lazy { getRandomLetter() + uuid[4] + uuid2[4] }
val JSBridgeResult by lazy { getRandomLetter() + uuid[5] + uuid2[5] }
val JS_URL by lazy {
"<script src=\"$nameUrl\"></script>"
}
val JS_INJECTION by lazy {
"""
const requestId = n => 'req_' + n + '_' + Date.now() + '_' + Math.random().toString(36).slice(-3);
const JSBridgeCallbacks = {};
const java = window.$nameJava;
delete window.$nameJava;
const source = window.$nameSource;
delete window.$nameSource;
const cache = window.$nameCache;
delete window.$nameCache;
function run(jsCode) {
return new Promise((resolve, reject) => {
const id = requestId("run");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("run", [String(jsCode)], id);
java?.request("run", [String(jsCode)], id);
});
};
function ajaxAwait(url, callTimeout) {
return new Promise((resolve, reject) => {
const id = requestId("ajaxAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("ajaxAwait", [String(url), String(callTimeout)], id);
java?.request("ajaxAwait", [String(url), String(callTimeout)], id);
});
};
function connectAwait(url, header, callTimeout) {
return new Promise((resolve, reject) => {
const id = requestId("connectAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("connectAwait", [String(url), String(header), String(callTimeout)], id);
java?.request("connectAwait", [String(url), String(header), String(callTimeout)], id);
});
};
function getAwait(url, header, callTimeout) {
return new Promise((resolve, reject) => {
const id = requestId("getAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("getAwait", [String(url), String(header), String(callTimeout)], id);
java?.request("getAwait", [String(url), String(header), String(callTimeout)], id);
});
};
function headAwait(url, header, callTimeout) {
return new Promise((resolve, reject) => {
const id = requestId("headAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("headAwait", [String(url), String(header), String(callTimeout)], id);
java?.request("headAwait", [String(url), String(header), String(callTimeout)], id);
});
};
function postAwait(url, body, header, callTimeout) {
return new Promise((resolve, reject) => {
const id = requestId("postAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("postAwait", [String(url), String(body), String(header), String(callTimeout)], id);
java?.request("postAwait", [String(url), String(body), String(header), String(callTimeout)], id);
});
};
function webViewAwait(html, url, js, cacheFirst) {
return new Promise((resolve, reject) => {
const id = requestId("webViewAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("webViewAwait", [String(html), String(url), String(js), String(cacheFirst)], id);
java?.request("webViewAwait", [String(html), String(url), String(js), String(cacheFirst)], id);
});
};
function webViewGetSourceAwait(html, url, js, sourceRegex, cacheFirst, delayTime) {
return new Promise((resolve, reject) => {
const id = requestId("webViewGetSourceAwait");
JSBridgeCallbacks[id] = { resolve, reject };
java?.request("webViewGetSourceAwait", [String(html), String(url), String(js), String(sourceRegex), String(cacheFirst), String(delayTime)], id);
});
}
function decryptStrAwait(transformation, key, iv, data) {
return new Promise((resolve, reject) => {
const id = requestId("decryptStrAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("decryptStrAwait", [String(transformation), String(key), String(iv), String(data)], id);
java?.request("decryptStrAwait", [String(transformation), String(key), String(iv), String(data)], id);
});
};
function encryptBase64Await(transformation, key, iv, data) {
return new Promise((resolve, reject) => {
const id = requestId("encryptBase64Await");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("encryptBase64Await", [String(transformation), String(key), String(iv), String(data)], id);
java?.request("encryptBase64Await", [String(transformation), String(key), String(iv), String(data)], id);
});
};
function encryptHexAwait(transformation, key, iv, data) {
return new Promise((resolve, reject) => {
const id = requestId("encryptHexAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("encryptHexAwait", [String(transformation), String(key), String(iv), String(data)], id);
java?.request("encryptHexAwait", [String(transformation), String(key), String(iv), String(data)], id);
});
};
function createSignHexAwait(algorithm, publicKey, privateKey, data) {
return new Promise((resolve, reject) => {
const id = requestId("createSignHexAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("createSignHexAwait", [String(algorithm), String(publicKey), String(privateKey), String(data)], id);
java?.request("createSignHexAwait", [String(algorithm), String(publicKey), String(privateKey), String(data)], id);
});
};
function downloadFileAwait(url) {
return new Promise((resolve, reject) => {
const id = requestId("downloadFileAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("downloadFileAwait", [String(url)], id);
java?.request("downloadFileAwait", [String(url)], id);
});
};
function readTxtFileAwait(path) {
return new Promise((resolve, reject) => {
const id = requestId("readTxtFileAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("readTxtFileAwait", [String(path)], id);
java?.request("readTxtFileAwait", [String(path)], id);
});
};
function importScriptAwait(url) {
return new Promise((resolve, reject) => {
const id = requestId("importScriptAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("importScriptAwait", [String(url)], id);
java?.request("importScriptAwait", [String(url)], id);
});
};
function getStringAwait(ruleStr, mContent) {
return new Promise((resolve, reject) => {
const id = requestId("getStringAwait");
JSBridgeCallbacks[id] = { resolve, reject };
window.$nameJava?.request("getStringAwait", [String(ruleStr), String(mContent)], id);
java?.request("getStringAwait", [String(ruleStr), String(mContent)], id);
});
};
window.$JSBridgeResult = function(requestId, result, error) {
@@ -354,31 +366,55 @@ class WebJsExtensions(
}
delete JSBridgeCallbacks[requestId];
}
};"""
};""".trimIndent()
}
val JS_INJECTION2 by lazy {
"""
const requestId = n => 'req_' + n + '_' + Date.now() + '_' + Math.random().toString(36).slice(-3);
const JSBridgeCallbacks = {};
const java = window.$nameJava;
delete window.$nameJava;
function run(jsCode) {
return new Promise((resolve, reject) => {
const id = requestId("run");
JSBridgeCallbacks[id] = { resolve, reject };
java?.request("run", [String(jsCode)], id);
});
};
window.$JSBridgeResult = function(requestId, result, error) {
if (JSBridgeCallbacks[requestId]) {
if (error) {
JSBridgeCallbacks[requestId].reject(error);
} else {
JSBridgeCallbacks[requestId].resolve(result);
}
delete JSBridgeCallbacks[requestId];
}
};""".trimIndent()
}
val basicJs by lazy {
"""
(function() {
if (screen.orientation && !screen.orientation.__patched) {
if (screen.orientation) {
screen.orientation.lock = function(orientation) {
return new Promise((resolve, reject) => {
window.$nameBasic?.lockOrientation(orientation)
window.$nameBasic?.lockOrientation(orientation);
resolve()
});
};
screen.orientation.unlock = function() {
return new Promise((resolve, reject) => {
window.$nameBasic?.lockOrientation('unlock')
window.$nameBasic?.lockOrientation('unlock');
resolve()
});
};
screen.orientation.__patched = true;
};
window.close = function() {
window.$nameBasic?.onCloseRequested();
};
})();"""
})();""".trimIndent()
}
}
}
@@ -255,6 +255,7 @@ class ReadBookActivity : BaseReadBookActivity(),
override val pageFactory get() = binding.readView.pageFactory
override val pageDelegate get() = binding.readView.pageDelegate
override val headerHeight: Int get() = binding.readView.curPage.headerHeight
override val imgBgPaddingStart: Int get() = binding.readView.curPage.imgBgPaddingStart
private val nextPageDebounce by lazy { Debounce { keyPage(PageDirection.NEXT) } }
private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } }
private var bookChanged = false
@@ -165,6 +165,36 @@ class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config
}.requestInputMethod()
}
3 -> { // 正则表达式分段
alert(title = "设置正则分段规则") {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.inputType = InputType.TYPE_CLASS_TEXT
editView.setText(ReadBookConfig.titleSegFlag)
editLayout.hint = "例如: [章回篇] 或 (第.{1,3}章)"
editView.isSingleLine = true
}
customView { alertBinding.root }
okButton {
val value = alertBinding.editView.text?.toString()?.trim()
if (!value.isNullOrEmpty()) {
try {
Regex(value)
ReadBookConfig.titleSegFlag = value
toastOnUi("正则规则已保存")
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
} catch (e: Exception) {
toastOnUi("正则表达式格式错误")
}
} else {
toastOnUi("规则不能为空")
}
}
cancelButton()
}.requestInputMethod()
}
else -> {
toastOnUi("当前分段模式无需配置参数")
}
@@ -64,6 +64,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
private var autoPager: AutoPager? = null
private var isScroll = false
private val renderRunnable by lazy { Runnable { preRenderPage() } }
private var lastClickTime = 0L
private var doubleClick = false
//绘制图片的paint
val imagePaint by lazy {
@@ -226,6 +228,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
column.selected = true
select(textPos)
}
is TextHtmlColumn -> {
if (!selectAble) return@touch
column.selected = true
select(textPos)
}
}
}
}
@@ -236,6 +243,14 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
*/
@Suppress("UNUSED_ANONYMOUS_PARAMETER")
fun click(x: Float, y: Float): Boolean {
val currentTime = System.currentTimeMillis()
val debounceClick = currentTime - lastClickTime < 300L //300毫秒防抖和双击
lastClickTime = currentTime
doubleClick = if (debounceClick) {
!doubleClick
} else {
false
}
var handled = false
touch(x, y) { _, textPos, textPage, textLine, column ->
when (column) {
@@ -254,45 +269,51 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
activity?.showDialogFragment(PhotoDialog(column.src))
handled = true
}
"2" -> { //兼容处理
if (ReadBook.book?.isOnLineTxt == true) {
val click = column.click
val src = column.src
if (!click.isNullOrBlank()) {
callBack.clickImg(click, src)
handled = true
} else {
handled = callBack.oldClickImg(src)
if (!debounceClick) {
if (ReadBook.book?.isOnLineTxt == true) {
val click = column.click
val src = column.src
if (!click.isNullOrBlank()) {
callBack.clickImg(click, src)
handled = true
} else {
handled = callBack.oldClickImg(src)
}
}
}
}
"3" -> { //关闭
handled = false
}
else -> { //默认点击
val click = column.click
if (!click.isNullOrBlank()) {
callBack.clickImg(click, column.src)
"4" -> { //双击
if (doubleClick) {
val click = column.click
if (!click.isNullOrBlank()) {
callBack.clickImg(click, column.src)
handled = true
}
} else {
handled = true
}
}
else -> { //默认点击
if (!debounceClick) {
val click = column.click
if (!click.isNullOrBlank()) {
callBack.clickImg(click, column.src)
handled = true
}
}
}
}
is TextHtmlColumn -> {
column.linkUrl?.let {
activity?.startActivity<OpenUrlConfirmActivity> {
putExtra("uri", it)
// putExtra("mimeType", mimeType)
// putExtra("sourceOrigin", source.getKey())
// putExtra("sourceName", source.getTag())
// putExtra("sourceType", source.getSourceType())
}
handled = true
}
}
}
}
@@ -308,7 +329,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
select: (textPos: TextPos) -> Unit,
) {
touchRough(x, y) { _, textPos, _, _, column ->
if (column is TextColumn) {
if (column is TextBaseColumn) {
column.selected = true
select(textPos)
}
@@ -582,7 +603,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
textPos.lineIndex = lineIndex
for ((charIndex, column) in textLine.columns.withIndex()) {
textPos.columnIndex = charIndex
if (column is TextColumn) {
if (column is TextBaseColumn) {
val compareStart = textPos.compare(selectStart)
val compareEnd = textPos.compare(selectEnd)
column.selected = compareStart >= 0 && compareEnd <= 0
@@ -600,13 +621,13 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
private fun upSelectedStart(x: Float, y: Float, top: Float) {
callBack.run {
upSelectedStart(x, y + headerHeight, top + headerHeight)
upSelectedStart(x + imgBgPaddingStart, y + headerHeight, top + headerHeight)
}
}
private fun upSelectedEnd(x: Float, y: Float) {
callBack.run {
upSelectedEnd(x, y + headerHeight)
upSelectedEnd(x + imgBgPaddingStart, y + headerHeight)
}
}
@@ -621,7 +642,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
val textPage = relativePage(relativePos)
textPage.lines.forEach { textLine ->
textLine.columns.forEach {
if (it is TextColumn) {
if (it is TextBaseColumn) {
it.selected = false
if (clearSearchResult) {
it.isSearchResult = false
@@ -649,7 +670,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
textPos.columnIndex = charIndex
val compareStart = textPos.compare(selectStart)
val compareEnd = textPos.compare(selectEnd)
if (column is TextColumn) {
if (column is TextBaseColumn) {
when {
compareStart == -1 -> if (
selectStart.columnIndex == textLine.columns.size
@@ -750,6 +771,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
interface CallBack {
val headerHeight: Int
val imgBgPaddingStart: Int
val pageFactory: TextPageFactory
val pageDelegate: PageDelegate?
val isScroll: Boolean
@@ -11,7 +11,6 @@ import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import io.legado.app.R
import io.legado.app.constant.AppConst.timeFormat
import io.legado.app.data.entities.Bookmark
@@ -72,6 +71,11 @@ class PageView(context: Context) : FrameLayout(context) {
return h1 + h2 + binding.vwRoot.paddingTop
}
val imgBgPaddingStart: Int
get() {
return binding.vwRoot.paddingStart
}
init {
upStyle()
binding.vwStatusBar.applyStatusBarPadding()
@@ -132,7 +136,7 @@ class PageView(context: Context) : FrameLayout(context) {
*/
fun upStatusBar() = with(binding.vwStatusBar) {
setPadding(paddingLeft, context.statusBarHeight, paddingRight, paddingBottom)
isGone = ReadBookConfig.hideStatusBar
isGone = ReadBookConfig.hideStatusBar || readBookActivity?.isInMultiWindow == true
}
fun upNavigationBar() {
@@ -12,6 +12,7 @@ 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.TextColumn
import io.legado.app.ui.book.read.page.provider.ChapterProvider
@@ -49,9 +50,8 @@ data class TextPage(
val lines: List<TextLine> get() = textLines
val lineSize: Int get() = textLines.size
val charSize: Int get() = text.length.coerceAtLeast(1)
val chapterPosition: Int
get() = textLines.firstOrNull()?.chapterPosition ?: 0
val searchResult = hashSetOf<TextColumn>()
val chapterPosition: Int get() = textLines.first().chapterPosition
val searchResult = hashSetOf<TextBaseColumn>()
var isMsgPage: Boolean = false
var canvasRecorder = CanvasRecorderFactory.create(true)
var doublePage = false
@@ -271,7 +271,7 @@ data class TextPage(
val columns = textLines[maxIndex].columns
for (index in 0 until columnIndex) {
val column = columns[index]
if (column is TextColumn) {
if (column is TextBaseColumn) {
length += column.charData.length
}
}
@@ -292,10 +292,10 @@ data class TextPage(
* @return
*/
fun containPos(chapterPos: Int): Boolean {
val line = lines.firstOrNull() ?: return false // lines 为空时直接返回 false
val line = lines.first()
val startPos = line.chapterPosition
val endPos = startPos + charSize
return chapterPos in startPos until endPos
return chapterPos in startPos..<endPos
}
fun draw(view: ContentTextView, canvas: Canvas, relativeOffset: Float) {
@@ -337,7 +337,10 @@ data class TextPage(
fun render(view: ContentTextView): Boolean {
if (!isCompleted) return false
return canvasRecorder.recordIfNeeded(view.width, renderHeight) {
return canvasRecorder.recordIfNeeded(
view.width,
renderHeight + 10.dpToPx()
) { //高度留余,避免图片过高时被截断 下划线最远10dp
drawPage(view, this)
}
}
@@ -365,15 +368,10 @@ data class TextPage(
}
fun upRenderHeight() {
if (lines.isEmpty()) {
renderHeight = 0
return
}
renderHeight = ceil(lines.last().lineBottom).toInt()
if (leftLineSize > 0 && leftLineSize <= lines.size) {
if (leftLineSize > 0 && leftLineSize != lines.size) {
val leftHeight = ceil(lines[leftLineSize - 1].lineBottom).toInt()
renderHeight = max(renderHeight, leftHeight)
}
}
}
@@ -20,13 +20,21 @@ object TitleStyleParser {
listOf(rawTitle.take(segDistance), rawTitle.substring(segDistance))
}
2 -> {
val flags = segFlag.split(",").map { it.trim() }.filter { it.isNotEmpty() }
if (flags.isEmpty()) listOf(rawTitle)
else {
val pattern = flags.joinToString("|") { Regex.escape(it) }
val regex = Regex("(?<=$pattern)")
rawTitle.split(regex).map { it.trim() }.filter { it.isNotEmpty() }
2, 3 -> {
if (segFlag.isEmpty()) {
listOf(rawTitle)
} else {
val regex = if (segType == 3) {
Regex("(?<=$segFlag)")
} else {
val flags = segFlag.split(",").map { it.trim() }.filter { it.isNotEmpty() }
val pattern = flags.joinToString("|") { Regex.escape(it) }
Regex("(?<=$pattern)")
}
rawTitle.split(regex)
.map { it.trim() }
.filter { it.isNotEmpty() }
}
}
@@ -17,29 +17,30 @@ import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import java.io.File
import java.lang.ref.WeakReference
@Suppress("unused")
class SourceLoginJsExtensions(
activity: AppCompatActivity?, source: BaseSource?,
private val bookType: Int = 0,
private val callback: Callback? = null
callback: Callback? = null
) : RssJsExtensions(activity, source) {
private val callbackRef: WeakReference<Callback> = WeakReference(callback)
interface Callback {
fun upUiData(data: Map<String, String?>?)
fun reUiView()
}
fun upLoginData(data: Map<String, String?>?) {
callback?.upUiData(data)
callbackRef.get()?.upUiData(data)
}
fun reLoginView() {
callback?.reUiView()
callbackRef.get()?.reUiView()
}
fun refreshExplore() {
callback?.reUiView()
callbackRef.get()?.reUiView()
}
fun refreshBookInfo() {
@@ -65,7 +66,12 @@ class SourceLoginJsExtensions(
}
@JvmOverloads
fun showBrowser(url: String, html: String, preloadJs: String? = null) {
fun showBrowser(
url: String,
html: String? = null,
preloadJs: String? = null,
config: String? = null
) {
val activity = activityRef.get() ?: return
val source = getSource() ?: return
activity.showDialogFragment(
@@ -74,7 +80,8 @@ class SourceLoginJsExtensions(
bookType,
url,
html,
preloadJs
preloadJs,
config
)
)
}
@@ -14,6 +14,7 @@ import io.legado.app.ui.book.explore.ExploreShowActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.rss.article.RssSortActivity
import io.legado.app.ui.widget.dialog.PhotoDialog
import io.legado.app.utils.isJsonObject
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.startActivity
@@ -25,6 +26,7 @@ import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.lang.ref.WeakReference
@Suppress("unused")
open class RssJsExtensions(activity: AppCompatActivity?, source: BaseSource?) : JsExtensions {
@@ -36,7 +38,7 @@ open class RssJsExtensions(activity: AppCompatActivity?, source: BaseSource?) :
}
override fun getTag(): String? {
return sourceRef.get()?.getTag()
return getSource()?.getTag()
}
@JavascriptInterface
@@ -67,6 +69,11 @@ open class RssJsExtensions(activity: AppCompatActivity?, source: BaseSource?) :
activityRef.get()?.showDialogFragment(AddToBookshelfDialog(bookUrl))
}
fun showPhoto(src: String) {
activityRef.get()?.showDialogFragment(PhotoDialog(src, getSource()?.getKey()))
}
@JavascriptInterface
@JvmOverloads
fun open(name: String, url: String? = null, title: String? = null, origin: String? = null) {
@@ -59,8 +59,9 @@ class BottomWebViewDialog() :
sourceKey: String,
bookType: Int,
url: String,
html: String,
preloadJs: String? = null
html: String? = null,
preloadJs: String? = null,
config: String? = null
) : this() {
arguments = Bundle().apply {
putString("sourceKey", sourceKey)
@@ -68,6 +69,7 @@ class BottomWebViewDialog() :
putString("url", url)
putString("html", html)
putString("preloadJs", preloadJs)
putString("config", config)
}
}