Преглед изворни кода

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

HapeLee пре 9 месеци
родитељ
комит
a76c25e450

+ 53 - 21
app/src/main/java/io/legado/app/help/JsExtensions.kt

@@ -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()
+    //}
 
-}
+}

+ 97 - 61
app/src/main/java/io/legado/app/help/webView/WebJsExtensions.kt

@@ -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,171 @@ 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) {
+                if (JSBridgeCallbacks[requestId]) {
+                    if (error) {
+                        JSBridgeCallbacks[requestId].reject(error);
+                    } else {
+                        JSBridgeCallbacks[requestId].resolve(result);
+                    }
+                    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) {
@@ -354,31 +391,30 @@ class WebJsExtensions(
                     }
                     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()
         }
     }
 }

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt

@@ -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

+ 30 - 0
app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt

@@ -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("当前分段模式无需配置参数")
                 }

+ 49 - 27
app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt

@@ -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

+ 6 - 2
app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt

@@ -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() {

+ 11 - 13
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt

@@ -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)
         }
     }
-
 }

+ 15 - 7
app/src/main/java/io/legado/app/ui/book/read/page/provider/TitleStyleParser.kt

@@ -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() }
                 }
             }
 

+ 14 - 7
app/src/main/java/io/legado/app/ui/login/SourceLoginJsExtensions.kt

@@ -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
             )
         )
     }

+ 8 - 1
app/src/main/java/io/legado/app/ui/rss/read/RssJsExtensions.kt

@@ -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) {

+ 4 - 2
app/src/main/java/io/legado/app/ui/widget/dialog/BottomWebViewDialog.kt

@@ -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)
         }
     }
 

+ 2 - 2
modules/rhino/src/main/java/com/script/rhino/RhinoExtensions.kt

@@ -1,6 +1,7 @@
 package com.script.rhino
 
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.supervisorScope
 import org.mozilla.javascript.Context
@@ -9,7 +10,6 @@ import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
 import kotlin.coroutines.ContinuationInterceptor
 import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.coroutineContext
 
 val rhinoContext: RhinoContext
     get() = Context.getCurrentContext() as RhinoContext
@@ -55,7 +55,7 @@ inline fun <T> runScriptWithContext(context: CoroutineContext, block: () -> T):
 suspend inline fun <T> runScriptWithContext(block: () -> T): T {
     val rhinoContext = Context.enter() as RhinoContext
     val previousCoroutineContext = rhinoContext.coroutineContext
-    rhinoContext.coroutineContext = coroutineContext.minusKey(ContinuationInterceptor)
+    rhinoContext.coroutineContext = currentCoroutineContext().minusKey(ContinuationInterceptor)
     try {
         return block()
     } finally {