diff --git a/app/src/main/java/io/legado/app/help/JsExtensions.kt b/app/src/main/java/io/legado/app/help/JsExtensions.kt index 66ffe8ffa..2c27c0655 100644 --- a/app/src/main/java/io/legado/app/help/JsExtensions.kt +++ b/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): Array { return ajaxAll(urlList, false) } - fun ajaxAll(urlList: Array, skipRateLimit: Boolean): Array { return runBlocking(context) { urlList.asFlow().mapAsync(AppConfig.threadCount) { url -> @@ -148,7 +146,6 @@ interface JsExtensions : JsEncodeUtils { fun ajaxTestAll(urlList: Array, timeout: Int): Array { return ajaxTestAll(urlList, timeout, false) } - fun ajaxTestAll( urlList: Array, 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 { - return ReadBookConfig.durConfig.toMap() - } */ + //fun getReadBookConfigMap(): Map { + // 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 { - return ThemeConfig.getDurConfig(appCtx).toMap() - } */ + //fun getThemeConfigMap(): Map { + // return ThemeConfig.getDurConfig(appCtx).toMap() + //} -} +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/webView/WebJsExtensions.kt b/app/src/main/java/io/legado/app/help/webView/WebJsExtensions.kt index 57f35ba49..0c0e5c9b2 100644 --- a/app/src/main/java/io/legado/app/help/webView/WebJsExtensions.kt +++ b/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 = 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>(headers).getOrNull() ?: emptyMap() return super.get(urlStr, headerMap, 9000).body() } - @JavascriptInterface fun get(urlStr: String, headers: String, timeout: Int?): String { val headerMap = GSON.fromJsonObject>(headers).getOrNull() ?: emptyMap() return super.get(urlStr, headerMap, timeout).body() } - @JavascriptInterface fun post(urlStr: String, body: String, headers: String): String { val headerMap = GSON.fromJsonObject>(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>(headers).getOrNull() ?: emptyMap() return super.post(urlStr, body, headerMap, timeout).body() } - @JavascriptInterface fun head(urlStr: String, headers: String): String { val headerMap = GSON.fromJsonObject>(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>(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 { + "" + } + 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() } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 8cdd5ca55..2d547f10d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt index 70198dc21..9f881969d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt +++ b/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("当前分段模式无需配置参数") } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt index d39ae4187..f5c5a9f43 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt +++ b/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 { 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 diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt index cd43549f3..eeebdcb23 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt +++ b/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() { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt index e6772f160..a213c316f 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt +++ b/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 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() + val chapterPosition: Int get() = textLines.first().chapterPosition + val searchResult = hashSetOf() 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.. 0 && leftLineSize <= lines.size) { + if (leftLineSize > 0 && leftLineSize != lines.size) { val leftHeight = ceil(lines[leftLineSize - 1].lineBottom).toInt() renderHeight = max(renderHeight, leftHeight) } } - } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TitleStyleParser.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TitleStyleParser.kt index bffcd27a3..d7ab91bbf 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TitleStyleParser.kt +++ b/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() } } } diff --git a/app/src/main/java/io/legado/app/ui/login/SourceLoginJsExtensions.kt b/app/src/main/java/io/legado/app/ui/login/SourceLoginJsExtensions.kt index 1a62a6993..91bc7d9ff 100644 --- a/app/src/main/java/io/legado/app/ui/login/SourceLoginJsExtensions.kt +++ b/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 = WeakReference(callback) interface Callback { fun upUiData(data: Map?) fun reUiView() } fun upLoginData(data: Map?) { - 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 ) ) } diff --git a/app/src/main/java/io/legado/app/ui/rss/read/RssJsExtensions.kt b/app/src/main/java/io/legado/app/ui/rss/read/RssJsExtensions.kt index f18456aa1..04a5ee1bf 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/RssJsExtensions.kt +++ b/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) { diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/BottomWebViewDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/BottomWebViewDialog.kt index 7d0262306..a4da94930 100644 --- a/app/src/main/java/io/legado/app/ui/widget/dialog/BottomWebViewDialog.kt +++ b/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) } } diff --git a/modules/rhino/src/main/java/com/script/rhino/RhinoExtensions.kt b/modules/rhino/src/main/java/com/script/rhino/RhinoExtensions.kt index f1ce0835a..e95867470 100644 --- a/modules/rhino/src/main/java/com/script/rhino/RhinoExtensions.kt +++ b/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 runScriptWithContext(context: CoroutineContext, block: () -> T): suspend inline fun 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 {