diff --git a/app/src/main/assets/help/appHelp.md b/app/src/main/assets/help/appHelp.md index 78dd3e976..c58c91b55 100644 --- a/app/src/main/assets/help/appHelp.md +++ b/app/src/main/assets/help/appHelp.md @@ -5,7 +5,7 @@ 【温馨提醒】 *本帮助可以在我的-右上角帮助按钮再次打开,更新前一定要做好备份,以免数据丢失!* 1. 为什么第一次安装好之后什么东西都没有? -* 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号[开源阅读]()、QQ群、酷安评论里获取由书友制作分享的书源。 +* 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号**[开源阅读]**、QQ群、酷安评论里获取由书友制作分享的书源。 2. 正文出现缺字漏字、内容缺失、排版错乱等情况,如何处理? * 有可能是净化规则出现问题,先关闭替换净化并刷新,再观察是否正常。如果正常说明净化规则存在误杀,如果关闭后仍然出现相关问题,请点击源链接查看原文与正文是否相同,如果不同,再进行反馈。 diff --git a/app/src/main/assets/help/sourceHelp.md b/app/src/main/assets/help/sourceHelp.md new file mode 100644 index 000000000..90ef9fde5 --- /dev/null +++ b/app/src/main/assets/help/sourceHelp.md @@ -0,0 +1,20 @@ +# 源规则帮助 + +* [书源帮助文档](https://alanskycn.gitee.io/teachme/Rule/source.html) +* [订阅源帮助文档](https://alanskycn.gitee.io/teachme/Rule/rss.html) + +* 规则标志, {{......}}内使用规则必须有明显的规则标志,没有规则标志当作js执行 + * @@ 默认规则,直接写时可以省略@@ + * @XPath: xpath规则,直接写时以//开头可省略@XPath + * @Json: json规则,直接写时以$.开头可省略@Json + * : regex规则,不可省略,只可以用在书籍列表和目录列表 + +* js 变量和函数 + * java 变量-当前类 + * baseUrl 变量-当前url,String + * result 变量-上一步的结果 + * book 变量-书籍类,方法见 io.legado.app.data.entities.Book + * cookie 变量-cookie操作类,方法见 io.legado.app.help.http.CookieStore + * cache 变量-缓存操作类,方法见 io.legado.app.help.CacheManager + * chapter 变量-当前目录类,方法见 io.legado.app.data.entities.BookChapter + * title 变量-当前标题,String \ No newline at end of file diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md index da8aa319c..ca3b56d6a 100644 --- a/app/src/main/assets/updateLog.md +++ b/app/src/main/assets/updateLog.md @@ -1,8 +1,13 @@ # 更新日志 -* 关注公众号 **[开源阅读]()** 菜单•软件下载 提前享受新版本。 -* 关注合作公众号 **[小说拾遗]()** 获取好看的小说。 +* 关注公众号 **[开源阅读]** 菜单•软件下载 提前享受新版本。 +* 关注合作公众号 **[小说拾遗]** 获取好看的小说。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 +**2020/11/22** +* 正文添加正确字体规则,可以通过文字轮廓对比将错误的文字替换为正确的文字 +* js添加java.readFile("path")读取本地文件,返回BiteArray +* js添加java.queryTTF(font: ByteArray?),返回字体处理类,可以编码和轮廓互查,io.legado.app.model.analyzeRule.QueryTTF + **2020/11/18** * 优化导航栏 * js添加java.log(msg: String)用于调试时输出消息 diff --git a/app/src/main/java/io/legado/app/constant/AppConst.kt b/app/src/main/java/io/legado/app/constant/AppConst.kt index 5cbb5a48b..835862fc0 100644 --- a/app/src/main/java/io/legado/app/constant/AppConst.kt +++ b/app/src/main/java/io/legado/app/constant/AppConst.kt @@ -36,9 +36,9 @@ object AppConst { val keyboardToolChars: List by lazy { arrayListOf( - "❓", "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", - "$", "#", "!", ".", "href", "src", "textNodes", "xpath", "json", "css", - "id", "class", "tag" + "❓", "@", "&", "|", "%", "/", "\\", ":", "[", "]", "{", "}", "<", ">", + "$", "#", "!", ".", "+", "-", "*", "=", "href", "src", "textNodes", "xpath", "json", + "css", "id", "class", "tag" ) } diff --git a/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt index 054939d0b..efe652657 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt @@ -9,8 +9,6 @@ data class ContentRule( var nextContentUrl: String? = null, var webJs: String? = null, var sourceRegex: String? = null, - var replaceRegex: String? = null, + var replaceRegex: String? = null, //正文获取后处理规则 var imageStyle: String? = null, //默认大小居中,FULL最大宽度 - var font: String? = null, //网页内包含的字体必须返回ByteArray - var correctFont: String? = null, //正确的字体必须返回ByteArray ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/CacheManager.kt b/app/src/main/java/io/legado/app/help/CacheManager.kt index 53039d4cc..152695e6c 100644 --- a/app/src/main/java/io/legado/app/help/CacheManager.kt +++ b/app/src/main/java/io/legado/app/help/CacheManager.kt @@ -2,18 +2,29 @@ package io.legado.app.help import io.legado.app.App import io.legado.app.data.entities.Cache +import io.legado.app.model.analyzeRule.QueryTTF +import io.legado.app.utils.ACache @Suppress("unused") object CacheManager { + private val queryTTFMap = hashMapOf>() + /** * saveTime 单位为秒 */ @JvmOverloads fun put(key: String, value: Any, saveTime: Int = 0) { - val deadline = if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000 - val cache = Cache(key, value.toString(), deadline) - App.db.cacheDao().insert(cache) + val deadline = + if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000 + when (value) { + is QueryTTF -> queryTTFMap[key] = Pair(deadline, value) + is ByteArray -> ACache.get(App.INSTANCE).put(key, value, saveTime) + else -> { + val cache = Cache(key, value.toString(), deadline) + App.db.cacheDao().insert(cache) + } + } } fun get(key: String): String? { @@ -36,4 +47,15 @@ object CacheManager { return get(key)?.toFloatOrNull() } + fun getByteArray(key: String): ByteArray? { + return ACache.get(App.INSTANCE).getAsBinary(key) + } + + fun getQueryTTF(key: String): QueryTTF? { + val cache = queryTTFMap[key] ?: return null + if (cache.first == 0L || cache.first > System.currentTimeMillis()) { + return cache.second + } + return null + } } \ No newline at end of file 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 95cc17d94..99500604f 100644 --- a/app/src/main/java/io/legado/app/help/JsExtensions.kt +++ b/app/src/main/java/io/legado/app/help/JsExtensions.kt @@ -1,14 +1,18 @@ package io.legado.app.help +import android.net.Uri import android.util.Base64 import androidx.annotation.Keep +import io.legado.app.App import io.legado.app.constant.AppConst.dateFormat import io.legado.app.help.http.CookieStore +import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.SSLHelper import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.QueryTTF import io.legado.app.utils.* +import kotlinx.coroutines.runBlocking import org.jsoup.Connection import org.jsoup.Jsoup import java.io.File @@ -230,11 +234,51 @@ interface JsExtensions { /** * 解析字体,返回字体解析类 */ - fun queryTTF(font: ByteArray?): QueryTTF? { + fun queryBase64TTF(base64: String?): QueryTTF? { + base64DecodeToByteArray(base64)?.let { + return QueryTTF(it) + } + return null + } + + fun queryTTF(path: String): QueryTTF? { + val qTTF = CacheManager.getQueryTTF(path) + if (qTTF != null) { + return qTTF + } + val font: ByteArray? = when { + path.isAbsUrl() -> runBlocking { + var x = CacheManager.getByteArray(path) + if (x == null) { + x = HttpHelper.simpleGetBytesAsync(path) + x?.let { + CacheManager.put(path, it) + } + } + return@runBlocking x + } + path.isContentScheme() -> { + Uri.parse(path).readBytes(App.INSTANCE) + } + else -> { + File(path).readBytes() + } + } font ?: return null return QueryTTF(font) } + fun replaceFont(text: String, font1: QueryTTF, font2: QueryTTF, start: Int, end: Int): String { + val contentArray = text.toCharArray() + contentArray.forEachIndexed { index, s -> + if (s > start.toChar() && s < end.toChar()) { + val code = font2.GetCodeByGlyf(font1.GetGlyfByCode(s.toInt())) + contentArray[index] = code.toChar() + } + } + return contentArray.joinToString("") + } + /** * 输出调试日志 */ diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt index 50fceacc4..e0a4dbb8c 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -39,6 +39,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { private var objectChangedJS = false private var objectChangedJP = false + @JvmOverloads fun setContent(content: Any?, baseUrl: String? = null): AnalyzeRule { if (content == null) throw AssertionError("Content cannot be null") this.content = content @@ -297,8 +298,8 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { return ArrayList() } - fun getByteArray(ruleStr: String): ByteArray? { - if (ruleStr.isEmpty()) return null + fun getContent(ruleStr: String, text: String?): String { + if (ruleStr.isEmpty()) return "" val ruleList = splitSourceRule(ruleStr) var result: Any? = null content?.let { o -> @@ -311,7 +312,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { result.toString(), sourceRule.rule.splitNotBlank("&&") ) - Mode.Js -> evalJS(sourceRule.rule, result) + Mode.Js -> evalJS(sourceRule.rule, result, text) Mode.Json -> getAnalyzeByJSonPath(it).getList(sourceRule.rule) Mode.XPath -> getAnalyzeByXPath(it).getElements(sourceRule.rule) else -> getAnalyzeByJSoup(it).getElements(sourceRule.rule) @@ -322,7 +323,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { } } } - return result as? ByteArray + return result?.toString() ?: "" } /** @@ -648,7 +649,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { /** * 执行JS */ - private fun evalJS(jsStr: String, result: Any?): Any? { + private fun evalJS(jsStr: String, result: Any?, content: String? = null): Any? { val bindings = SimpleBindings() bindings["java"] = this bindings["cookie"] = CookieStore @@ -658,6 +659,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { bindings["baseUrl"] = baseUrl bindings["chapter"] = chapter bindings["title"] = chapter?.title + bindings["content"] = content return SCRIPT_ENGINE.eval(jsStr, bindings) } diff --git a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt index 3c68bece1..f13df3605 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt @@ -10,10 +10,8 @@ import io.legado.app.help.BookHelp import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeUrl -import io.legado.app.model.analyzeRule.QueryTTF import io.legado.app.utils.NetworkUtils import io.legado.app.utils.htmlFormat -import io.legado.app.utils.toStringArray import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext @@ -37,21 +35,6 @@ object BookContent { val nextUrlList = arrayListOf(baseUrl) val contentRule = bookSource.getContentRule() val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl) - val fontRule = contentRule.font - val correctFontRule = contentRule.correctFont - var font: ByteArray? = null - var correctFont: ByteArray? = null - fontRule?.let { - //todo 获取网页嵌入字体 - font = analyzeRule.getByteArray(it) - } - correctFontRule?.let { - //todo 获取正确字体 - correctFont = analyzeRule.getByteArray(it) - } - if (correctFont == null && font != null) { - BookHelp.saveFont(book, bookChapter, font!!) - } var contentData = analyzeContent( book, baseUrl, body, contentRule, bookChapter, bookSource ) @@ -111,21 +94,7 @@ object BookContent { var contentStr = content.toString().htmlFormat() val replaceRegex = bookSource.ruleContent?.replaceRegex if (!replaceRegex.isNullOrEmpty()) { - analyzeRule.setContent(contentStr).setBaseUrl(baseUrl) - analyzeRule.chapter = bookChapter - contentStr = analyzeRule.getString(replaceRegex) - } - if (correctFont != null && font != null) { - val queryTTF = QueryTTF(font!!) - val cQueryTTF = QueryTTF(correctFont!!) - val contentArray = contentStr.toCharArray() - contentArray.forEachIndexed { index, s -> - if(s> 58000.toChar()){ - val code = cQueryTTF.GetCodeByGlyf(queryTTF.GetGlyfByCode(s.toInt())) - contentArray[index] = code.toChar() - } - } - contentStr = contentArray.joinToString("") + contentStr = analyzeRule.getContent(replaceRegex, contentStr) } Debug.log(bookSource.bookSourceUrl, "┌获取章节名称") Debug.log(bookSource.bookSourceUrl, "└${bookChapter.title}") diff --git a/app/src/main/java/io/legado/app/service/help/ReadBook.kt b/app/src/main/java/io/legado/app/service/help/ReadBook.kt index 561155843..4c022c653 100644 --- a/app/src/main/java/io/legado/app/service/help/ReadBook.kt +++ b/app/src/main/java/io/legado/app/service/help/ReadBook.kt @@ -191,12 +191,7 @@ object ReadBook { if (book != null && textChapter != null) { val key = IntentDataHelp.putData(textChapter) ReadAloud.play( - App.INSTANCE, - book.name, - textChapter.title, - durPageIndex, - key, - play + App.INSTANCE, book.name, textChapter.title, durPageIndex, key, play ) } } @@ -275,10 +270,7 @@ object ReadBook { CacheBook.download(webBook, book, chapter) } else if (book != null) { contentLoadFinish( - book, - chapter, - "没有书源", - resetPageOffset = resetPageOffset + book, chapter, "没有书源", resetPageOffset = resetPageOffset ) removeLoading(chapter.index) } else { @@ -383,11 +375,7 @@ object ReadBook { durChapterIndex -> { curTextChapter = ChapterProvider.getTextChapter( - book, - chapter, - contents, - chapterSize, - imageStyle + book, chapter, contents, chapterSize, imageStyle ) if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset) callBack?.upView() @@ -397,22 +385,14 @@ object ReadBook { durChapterIndex - 1 -> { prevTextChapter = ChapterProvider.getTextChapter( - book, - chapter, - contents, - chapterSize, - imageStyle + book, chapter, contents, chapterSize, imageStyle ) if (upContent) callBack?.upContent(-1, resetPageOffset) } durChapterIndex + 1 -> { nextTextChapter = ChapterProvider.getTextChapter( - book, - chapter, - contents, - chapterSize, - imageStyle + book, chapter, contents, chapterSize, imageStyle ) if (upContent) callBack?.upContent(1, resetPageOffset) } 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 dc155cd10..227d6b9a7 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 @@ -37,7 +37,7 @@ import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR import io.legado.app.ui.book.read.page.ContentTextView import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.TextPageFactory -import io.legado.app.ui.book.read.page.delegate.PageDelegate +import io.legado.app.ui.book.read.page.entities.PageDirection import io.legado.app.ui.book.searchContent.SearchContentActivity import io.legado.app.ui.book.toc.ChapterListActivity import io.legado.app.ui.login.SourceLogin @@ -260,28 +260,28 @@ class ReadBookActivity : ReadBookBaseActivity(), when { isPrevKey(keyCode) -> { if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { - page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.PREV) + page_view.pageDelegate?.keyTurnPage(PageDirection.PREV) return true } } isNextKey(keyCode) -> { if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { - page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) + page_view.pageDelegate?.keyTurnPage(PageDirection.NEXT) return true } } keyCode == KeyEvent.KEYCODE_VOLUME_UP -> { - if (volumeKeyPage(PageDelegate.Direction.PREV)) { + if (volumeKeyPage(PageDirection.PREV)) { return true } } keyCode == KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (volumeKeyPage(PageDelegate.Direction.NEXT)) { + if (volumeKeyPage(PageDirection.NEXT)) { return true } } keyCode == KeyEvent.KEYCODE_SPACE -> { - page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) + page_view.pageDelegate?.keyTurnPage(PageDirection.NEXT) return true } } @@ -307,7 +307,7 @@ class ReadBookActivity : ReadBookBaseActivity(), override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { when (keyCode) { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (volumeKeyPage(PageDelegate.Direction.NONE)) { + if (volumeKeyPage(PageDirection.NONE)) { return true } } @@ -458,7 +458,7 @@ class ReadBookActivity : ReadBookBaseActivity(), /** * 音量键翻页 */ - private fun volumeKeyPage(direction: PageDelegate.Direction): Boolean { + private fun volumeKeyPage(direction: PageDirection): Boolean { if (!read_menu.isVisible) { if (getPrefBoolean("volumeKeyPage", true)) { if (getPrefBoolean("volumeKeyPageOnPlay") @@ -612,7 +612,7 @@ class ReadBookActivity : ReadBookBaseActivity(), autoPageProgress++ if (autoPageProgress >= ReadBookConfig.autoReadSpeed * 50) { autoPageProgress = 0 - page_view.fillPage(PageDelegate.Direction.NEXT) + page_view.fillPage(PageDirection.NEXT) } else { page_view.invalidate() } 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 4abc569e7..ae5edd4f5 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 @@ -15,6 +15,7 @@ import io.legado.app.help.ReadBookConfig import io.legado.app.lib.theme.accentColor import io.legado.app.service.help.ReadBook import io.legado.app.ui.book.read.page.delegate.* +import io.legado.app.ui.book.read.page.entities.PageDirection import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.utils.activity @@ -339,12 +340,12 @@ class PageView(context: Context, attrs: AttributeSet) : curPage.cancelSelect() } - fun fillPage(direction: PageDelegate.Direction) { + fun fillPage(direction: PageDirection) { when (direction) { - PageDelegate.Direction.PREV -> { + PageDirection.PREV -> { pageFactory.moveToPrev(true) } - PageDelegate.Direction.NEXT -> { + PageDirection.NEXT -> { pageFactory.moveToNext(true) } else -> Unit diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt index 278b8fffc..5a32fda2f 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt @@ -4,6 +4,7 @@ import android.graphics.Canvas import android.graphics.Matrix import android.graphics.drawable.GradientDrawable import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.entities.PageDirection class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { private val bitmapMatrix = Matrix() @@ -21,19 +22,19 @@ class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { if (!isRunning) return val offsetX = touchX - startX - if ((mDirection == Direction.NEXT && offsetX > 0) - || (mDirection == Direction.PREV && offsetX < 0) + if ((mDirection == PageDirection.NEXT && offsetX > 0) + || (mDirection == PageDirection.PREV && offsetX < 0) ) { return } val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth - if (mDirection == Direction.PREV) { + if (mDirection == PageDirection.PREV) { bitmapMatrix.setTranslate(distanceX, 0.toFloat()) curBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } addShadow(distanceX.toInt(), canvas) - } else if (mDirection == Direction.NEXT) { + } else if (mDirection == PageDirection.NEXT) { bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat()) nextBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } @@ -60,7 +61,7 @@ class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { override fun onAnimStart(animationSpeed: Int) { val distanceX: Float when (mDirection) { - Direction.NEXT -> distanceX = + PageDirection.NEXT -> distanceX = if (isCancel) { var dis = viewWidth - startX + touchX if (dis > viewWidth) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt index 356d7669c..e6d5d9ee5 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt @@ -3,6 +3,7 @@ package io.legado.app.ui.book.read.page.delegate import android.graphics.Bitmap import android.view.MotionEvent import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.entities.PageDirection import io.legado.app.utils.screenshot abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageView) { @@ -11,20 +12,20 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie protected var prevBitmap: Bitmap? = null protected var nextBitmap: Bitmap? = null - override fun setDirection(direction: Direction) { + override fun setDirection(direction: PageDirection) { super.setDirection(direction) setBitmap() } private fun setBitmap() { when (mDirection) { - Direction.PREV -> { + PageDirection.PREV -> { prevBitmap?.recycle() prevBitmap = prevPage.screenshot() curBitmap?.recycle() curBitmap = curPage.screenshot() } - Direction.NEXT -> { + PageDirection.NEXT -> { nextBitmap?.recycle() nextBitmap = nextPage.screenshot() curBitmap?.recycle() @@ -79,19 +80,19 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie noNext = true return } - setDirection(Direction.PREV) + setDirection(PageDirection.PREV) } else { //如果不存在表示没有下一页了 if (!hasNext()) { noNext = true return } - setDirection(Direction.NEXT) + setDirection(PageDirection.NEXT) } } } if (isMoved) { - isCancel = if (mDirection == Direction.NEXT) sumX > lastX else sumX < lastX + isCancel = if (mDirection == PageDirection.NEXT) sumX > lastX else sumX < lastX isRunning = true //设置触摸点 pageView.setTouchPoint(sumX, sumY) @@ -117,7 +118,7 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie override fun nextPageByAnim(animationSpeed: Int) { abortAnim() if (!hasNext()) return - setDirection(Direction.NEXT) + setDirection(PageDirection.NEXT) pageView.setTouchPoint(viewWidth.toFloat(), 0f, false) onAnimStart(animationSpeed) } @@ -125,7 +126,7 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie override fun prevPageByAnim(animationSpeed: Int) { abortAnim() if (!hasPrev()) return - setDirection(Direction.PREV) + setDirection(PageDirection.PREV) pageView.setTouchPoint(0f, 0f) onAnimStart(animationSpeed) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt index 3a2dabc1d..fba190df5 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt @@ -10,6 +10,7 @@ import com.google.android.material.snackbar.Snackbar import io.legado.app.R import io.legado.app.ui.book.read.page.ContentView import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.entities.PageDirection import kotlin.math.abs abstract class PageDelegate(protected val pageView: PageView) { @@ -47,7 +48,7 @@ abstract class PageDelegate(protected val pageView: PageView) { var noNext = true //移动方向 - var mDirection = Direction.NONE + var mDirection = PageDirection.NONE var isCancel = false var isRunning = false var isStarted = false @@ -117,17 +118,17 @@ abstract class PageDelegate(protected val pageView: PageView) { abstract fun prevPageByAnim(animationSpeed: Int) - open fun keyTurnPage(direction: Direction) { + open fun keyTurnPage(direction: PageDirection) { if (isRunning) return when (direction) { - Direction.NEXT -> nextPageByAnim(100) - Direction.PREV -> prevPageByAnim(100) + PageDirection.NEXT -> nextPageByAnim(100) + PageDirection.PREV -> prevPageByAnim(100) else -> return } } @CallSuper - open fun setDirection(direction: Direction) { + open fun setDirection(direction: PageDirection) { mDirection = direction } @@ -149,7 +150,7 @@ abstract class PageDelegate(protected val pageView: PageView) { //取消 isCancel = false //是下一章还是前一章 - setDirection(Direction.NONE) + setDirection(PageDirection.NONE) } /** @@ -184,8 +185,4 @@ abstract class PageDelegate(protected val pageView: PageView) { } - enum class Direction { - NONE, PREV, NEXT - } - } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt index 5d0e97ec6..5d58e48d6 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt @@ -6,6 +6,7 @@ import android.os.Build import android.view.MotionEvent import io.legado.app.help.ReadBookConfig import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.entities.PageDirection import kotlin.math.* @Suppress("DEPRECATION") @@ -123,13 +124,13 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi } MotionEvent.ACTION_MOVE -> { if ((startY > viewHeight / 3 && startY < viewHeight * 2 / 3) - || mDirection == Direction.PREV + || mDirection == PageDirection.PREV ) { pageView.touchY = viewHeight.toFloat() } if (startY > viewHeight / 3 && startY < viewHeight / 2 - && mDirection == Direction.NEXT + && mDirection == PageDirection.NEXT ) { pageView.touchY = 1f } @@ -137,17 +138,17 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi } } - override fun setDirection(direction: Direction) { + override fun setDirection(direction: PageDirection) { super.setDirection(direction) when (direction) { - Direction.PREV -> + PageDirection.PREV -> //上一页滑动不出现对角 if (startX > viewWidth / 2) { calcCornerXY(startX, viewHeight.toFloat()) } else { calcCornerXY(viewWidth - startX, viewHeight.toFloat()) } - Direction.NEXT -> + PageDirection.NEXT -> if (viewWidth / 2 > startX) { calcCornerXY(viewWidth - startX, startY) } @@ -160,12 +161,12 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi val dy: Float // dy 垂直方向滑动的距离,负值会使滚动向上滚动 if (isCancel) { - dx = if (mCornerX > 0 && mDirection == Direction.NEXT) { + dx = if (mCornerX > 0 && mDirection == PageDirection.NEXT) { (viewWidth - touchX) } else { -touchX } - if (mDirection != Direction.NEXT) { + if (mDirection != PageDirection.NEXT) { dx = -(viewWidth + touchX) } dy = if (mCornerY > 0) { @@ -174,7 +175,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi -touchY // 防止mTouchY最终变为0 } } else { - dx = if (mCornerX > 0 && mDirection == Direction.NEXT) { + dx = if (mCornerX > 0 && mDirection == PageDirection.NEXT) { -(viewWidth + touchX) } else { (viewWidth - touchX + viewWidth) @@ -197,14 +198,14 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi override fun onDraw(canvas: Canvas) { if (!isRunning) return when (mDirection) { - Direction.NEXT -> { + PageDirection.NEXT -> { calcPoints() drawCurrentPageArea(canvas, curBitmap) drawNextPageAreaAndShadow(canvas, nextBitmap) drawCurrentPageShadow(canvas) drawCurrentBackArea(canvas, curBitmap) } - Direction.PREV -> { + PageDirection.PREV -> { calcPoints() drawCurrentPageArea(canvas, prevBitmap) drawNextPageAreaAndShadow(canvas, curBitmap) diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt index 9c3413f6a..afb7ee499 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt @@ -3,6 +3,7 @@ package io.legado.app.ui.book.read.page.delegate import android.graphics.Canvas import android.graphics.Matrix import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.entities.PageDirection class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { @@ -11,7 +12,7 @@ class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { override fun onAnimStart(animationSpeed: Int) { val distanceX: Float when (mDirection) { - Direction.NEXT -> distanceX = + PageDirection.NEXT -> distanceX = if (isCancel) { var dis = viewWidth - startX + touchX if (dis > viewWidth) { @@ -34,17 +35,17 @@ class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { override fun onDraw(canvas: Canvas) { val offsetX = touchX - startX - if ((mDirection == Direction.NEXT && offsetX > 0) - || (mDirection == Direction.PREV && offsetX < 0) + if ((mDirection == PageDirection.NEXT && offsetX > 0) + || (mDirection == PageDirection.PREV && offsetX < 0) ) return val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth if (!isRunning) return - if (mDirection == Direction.PREV) { + if (mDirection == PageDirection.PREV) { bitmapMatrix.setTranslate(distanceX + viewWidth, 0.toFloat()) curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } bitmapMatrix.setTranslate(distanceX, 0.toFloat()) prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } - } else if (mDirection == Direction.NEXT) { + } else if (mDirection == PageDirection.NEXT) { bitmapMatrix.setTranslate(distanceX, 0.toFloat()) nextBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat()) diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/PageDirection.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/PageDirection.kt new file mode 100644 index 000000000..713c6bd67 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/PageDirection.kt @@ -0,0 +1,5 @@ +package io.legado.app.ui.book.read.page.entities + +enum class PageDirection { + NONE, PREV, NEXT +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt index fa4a12f0a..37423cc54 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt @@ -237,8 +237,6 @@ class BookSourceEditActivity : add(EditEntity("sourceRegex", cr?.sourceRegex, R.string.rule_source_regex)) add(EditEntity("replaceRegex", cr?.replaceRegex, R.string.rule_replace_regex)) add(EditEntity("imageStyle", cr?.imageStyle, R.string.rule_image_style)) - add(EditEntity("font", cr?.font, R.string.rule_font)) - add(EditEntity("correctFont", cr?.correctFont, R.string.rule_correct_font)) } //发现 val er = source?.getExploreRule() @@ -342,8 +340,6 @@ class BookSourceEditActivity : "sourceRegex" -> contentRule.sourceRegex = it.value "replaceRegex" -> contentRule.replaceRegex = it.value "imageStyle" -> contentRule.imageStyle = it.value - "font" -> contentRule.font = it.value - "correctFont" -> contentRule.correctFont = it.value } } source.ruleSearch = searchRule @@ -390,13 +386,18 @@ class BookSourceEditActivity : selector(getString(R.string.help), items) { _, index -> when (index) { 0 -> insertText(AppConst.urlOption) - 1 -> openUrl("https://alanskycn.gitee.io/teachme/Rule/source.html") + 1 -> showSourceHelp() 2 -> showRegexHelp() 3 -> FilePicker.selectFile(this, selectPathRequestCode) } } } + private fun showSourceHelp() { + val mdText = String(assets.open("help/sourceHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) + } + private fun showRegexHelp() { val mdText = String(assets.open("help/regexHelp.md").readBytes()) TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt index aa7a2db15..dbcc4b0b1 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt @@ -21,7 +21,10 @@ import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity import io.legado.app.ui.widget.KeyboardToolPop import io.legado.app.ui.widget.dialog.TextDialog -import io.legado.app.utils.* +import io.legado.app.utils.GSON +import io.legado.app.utils.getViewModel +import io.legado.app.utils.sendToClip +import io.legado.app.utils.shareWithQr import kotlinx.android.synthetic.main.activity_rss_source_edit.* import org.jetbrains.anko.* import kotlin.math.abs @@ -199,12 +202,17 @@ class RssSourceEditActivity : selector(getString(R.string.help), items) { _, index -> when (index) { 0 -> insertText(AppConst.urlOption) - 1 -> openUrl("https://alanskycn.gitee.io/teachme/Rule/rss.html") + 1 -> showSourceHelp() 2 -> showRegexHelp() } } } + private fun showSourceHelp() { + val mdText = String(assets.open("help/sourceHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) + } + private fun showRegexHelp() { val mdText = String(assets.open("help/regexHelp.md").readBytes()) TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) diff --git a/app/src/main/java/io/legado/app/ui/widget/text/BetterLinkMovementMethod.kt b/app/src/main/java/io/legado/app/ui/widget/text/BetterLinkMovementMethod.kt new file mode 100644 index 000000000..da4ea65eb --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/BetterLinkMovementMethod.kt @@ -0,0 +1,437 @@ +package io.legado.app.ui.widget.text + +import android.app.Activity +import android.graphics.RectF +import android.text.Selection +import android.text.Spannable +import android.text.Spanned +import android.text.method.LinkMovementMethod +import android.text.style.BackgroundColorSpan +import android.text.style.ClickableSpan +import android.text.style.URLSpan +import android.text.util.Linkify +import android.view.* +import android.widget.TextView +import io.legado.app.R +import io.legado.app.ui.widget.text.BetterLinkMovementMethod.LongPressTimer.OnTimerReachedListener + +class BetterLinkMovementMethod protected constructor() : LinkMovementMethod() { + private var onLinkClickListener: OnLinkClickListener? = null + private var onLinkLongClickListener: OnLinkLongClickListener? = null + private val touchedLineBounds = RectF() + private var isUrlHighlighted = false + private var clickableSpanUnderTouchOnActionDown: ClickableSpan? = null + private var activeTextViewHashcode = 0 + private var ongoingLongPressTimer: LongPressTimer? = null + private var wasLongPressRegistered = false + + interface OnLinkClickListener { + /** + * @param textView The TextView on which a click was registered. + * @param url The clicked URL. + * @return True if this click was handled. False to let Android handle the URL. + */ + fun onClick(textView: TextView?, url: String?): Boolean + } + + interface OnLinkLongClickListener { + /** + * @param textView The TextView on which a long-click was registered. + * @param url The long-clicked URL. + * @return True if this long-click was handled. False to let Android handle the URL (as a short-click). + */ + fun onLongClick(textView: TextView?, url: String?): Boolean + } + + /** + * Set a listener that will get called whenever any link is clicked on the TextView. + */ + fun setOnLinkClickListener(clickListener: OnLinkClickListener?): BetterLinkMovementMethod { + if (this === singleInstance) { + throw UnsupportedOperationException( + "Setting a click listener on the instance returned by getInstance() is not supported to avoid memory " + + "leaks. Please use newInstance() or any of the linkify() methods instead." + ) + } + onLinkClickListener = clickListener + return this + } + + /** + * Set a listener that will get called whenever any link is clicked on the TextView. + */ + fun setOnLinkLongClickListener(longClickListener: OnLinkLongClickListener?): BetterLinkMovementMethod { + if (this === singleInstance) { + throw UnsupportedOperationException( + "Setting a long-click listener on the instance returned by getInstance() is not supported to avoid " + + "memory leaks. Please use newInstance() or any of the linkify() methods instead." + ) + } + onLinkLongClickListener = longClickListener + return this + } + + override fun onTouchEvent(textView: TextView, text: Spannable, event: MotionEvent): Boolean { + if (activeTextViewHashcode != textView.hashCode()) { + // Bug workaround: TextView stops calling onTouchEvent() once any URL is highlighted. + // A hacky solution is to reset any "autoLink" property set in XML. But we also want + // to do this once per TextView. + activeTextViewHashcode = textView.hashCode() + textView.autoLinkMask = 0 + } + val clickableSpanUnderTouch = findClickableSpanUnderTouch(textView, text, event) + if (event.action == MotionEvent.ACTION_DOWN) { + clickableSpanUnderTouchOnActionDown = clickableSpanUnderTouch + } + val touchStartedOverAClickableSpan = clickableSpanUnderTouchOnActionDown != null + return when (event.action) { + MotionEvent.ACTION_DOWN -> { + clickableSpanUnderTouch?.let { highlightUrl(textView, it, text) } + if (touchStartedOverAClickableSpan && onLinkLongClickListener != null) { + val longClickListener: OnTimerReachedListener = + object : OnTimerReachedListener { + override fun onTimerReached() { + wasLongPressRegistered = true + textView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + removeUrlHighlightColor(textView) + dispatchUrlLongClick(textView, clickableSpanUnderTouch) + } + } + startTimerForRegisteringLongClick(textView, longClickListener) + } + touchStartedOverAClickableSpan + } + MotionEvent.ACTION_UP -> { + // Register a click only if the touch started and ended on the same URL. + if (!wasLongPressRegistered && touchStartedOverAClickableSpan && clickableSpanUnderTouch === clickableSpanUnderTouchOnActionDown) { + dispatchUrlClick(textView, clickableSpanUnderTouch) + } + cleanupOnTouchUp(textView) + + // Consume this event even if we could not find any spans to avoid letting Android handle this event. + // Android's TextView implementation has a bug where links get clicked even when there is no more text + // next to the link and the touch lies outside its bounds in the same direction. + touchStartedOverAClickableSpan + } + MotionEvent.ACTION_CANCEL -> { + cleanupOnTouchUp(textView) + false + } + MotionEvent.ACTION_MOVE -> { + // Stop listening for a long-press as soon as the user wanders off to unknown lands. + if (clickableSpanUnderTouch !== clickableSpanUnderTouchOnActionDown) { + removeLongPressCallback(textView) + } + if (!wasLongPressRegistered) { + // Toggle highlight. + if (clickableSpanUnderTouch != null) { + highlightUrl(textView, clickableSpanUnderTouch, text) + } else { + removeUrlHighlightColor(textView) + } + } + touchStartedOverAClickableSpan + } + else -> false + } + } + + private fun cleanupOnTouchUp(textView: TextView) { + wasLongPressRegistered = false + clickableSpanUnderTouchOnActionDown = null + removeUrlHighlightColor(textView) + removeLongPressCallback(textView) + } + + /** + * Determines the touched location inside the TextView's text and returns the ClickableSpan found under it (if any). + * + * @return The touched ClickableSpan or null. + */ + protected fun findClickableSpanUnderTouch( + textView: TextView, + text: Spannable, + event: MotionEvent + ): ClickableSpan? { + // So we need to find the location in text where touch was made, regardless of whether the TextView + // has scrollable text. That is, not the entire text is currently visible. + var touchX = event.x.toInt() + var touchY = event.y.toInt() + + // Ignore padding. + touchX -= textView.totalPaddingLeft + touchY -= textView.totalPaddingTop + + // Account for scrollable text. + touchX += textView.scrollX + touchY += textView.scrollY + val layout = textView.layout + val touchedLine = layout.getLineForVertical(touchY) + val touchOffset = layout.getOffsetForHorizontal(touchedLine, touchX.toFloat()) + touchedLineBounds.left = layout.getLineLeft(touchedLine) + touchedLineBounds.top = layout.getLineTop(touchedLine).toFloat() + touchedLineBounds.right = layout.getLineWidth(touchedLine) + touchedLineBounds.left + touchedLineBounds.bottom = layout.getLineBottom(touchedLine).toFloat() + return if (touchedLineBounds.contains(touchX.toFloat(), touchY.toFloat())) { + // Find a ClickableSpan that lies under the touched area. + val spans = text.getSpans(touchOffset, touchOffset, ClickableSpan::class.java) + for (span in spans) { + if (span is ClickableSpan) { + return span + } + } + // No ClickableSpan found under the touched location. + null + } else { + // Touch lies outside the line's horizontal bounds where no spans should exist. + null + } + } + + /** + * Adds a background color span at clickableSpan's location. + */ + protected fun highlightUrl(textView: TextView, clickableSpan: ClickableSpan?, text: Spannable) { + if (isUrlHighlighted) { + return + } + isUrlHighlighted = true + val spanStart = text.getSpanStart(clickableSpan) + val spanEnd = text.getSpanEnd(clickableSpan) + val highlightSpan = BackgroundColorSpan(textView.highlightColor) + text.setSpan(highlightSpan, spanStart, spanEnd, Spannable.SPAN_INCLUSIVE_INCLUSIVE) + textView.setTag(R.id.bettermovementmethod_highlight_background_span, highlightSpan) + Selection.setSelection(text, spanStart, spanEnd) + } + + /** + * Removes the highlight color under the Url. + */ + protected fun removeUrlHighlightColor(textView: TextView) { + if (!isUrlHighlighted) { + return + } + isUrlHighlighted = false + val text = textView.text as Spannable + val highlightSpan = + textView.getTag(R.id.bettermovementmethod_highlight_background_span) as BackgroundColorSpan + text.removeSpan(highlightSpan) + Selection.removeSelection(text) + } + + protected fun startTimerForRegisteringLongClick( + textView: TextView, + longClickListener: OnTimerReachedListener? + ) { + ongoingLongPressTimer = LongPressTimer() + ongoingLongPressTimer!!.setOnTimerReachedListener(longClickListener) + textView.postDelayed( + ongoingLongPressTimer, + ViewConfiguration.getLongPressTimeout().toLong() + ) + } + + /** + * Remove the long-press detection timer. + */ + protected fun removeLongPressCallback(textView: TextView) { + if (ongoingLongPressTimer != null) { + textView.removeCallbacks(ongoingLongPressTimer) + ongoingLongPressTimer = null + } + } + + protected fun dispatchUrlClick(textView: TextView, clickableSpan: ClickableSpan?) { + val clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan) + val handled = onLinkClickListener != null && onLinkClickListener!!.onClick( + textView, + clickableSpanWithText.text() + ) + if (!handled) { + // Let Android handle this click. + clickableSpanWithText.span()!!.onClick(textView) + } + } + + protected fun dispatchUrlLongClick(textView: TextView, clickableSpan: ClickableSpan?) { + val clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan) + val handled = onLinkLongClickListener != null && onLinkLongClickListener!!.onLongClick( + textView, + clickableSpanWithText.text() + ) + if (!handled) { + // Let Android handle this long click as a short-click. + clickableSpanWithText.span()!!.onClick(textView) + } + } + + protected class LongPressTimer : Runnable { + private var onTimerReachedListener: OnTimerReachedListener? = null + + interface OnTimerReachedListener { + fun onTimerReached() + } + + override fun run() { + onTimerReachedListener!!.onTimerReached() + } + + fun setOnTimerReachedListener(listener: OnTimerReachedListener?) { + onTimerReachedListener = listener + } + } + + /** + * A wrapper to support all [ClickableSpan]s that may or may not provide URLs. + */ + protected class ClickableSpanWithText protected constructor( + private val span: ClickableSpan?, + private val text: String + ) { + fun span(): ClickableSpan? { + return span + } + + fun text(): String { + return text + } + + companion object { + fun ofSpan(textView: TextView, span: ClickableSpan?): ClickableSpanWithText { + val s = textView.text as Spanned + val text: String + text = if (span is URLSpan) { + span.url + } else { + val start = s.getSpanStart(span) + val end = s.getSpanEnd(span) + s.subSequence(start, end).toString() + } + return ClickableSpanWithText(span, text) + } + } + } + + companion object { + private var singleInstance: BetterLinkMovementMethod? = null + private const val LINKIFY_NONE = -2 + + /** + * Return a new instance of BetterLinkMovementMethod. + */ + fun newInstance(): BetterLinkMovementMethod { + return BetterLinkMovementMethod() + } + + /** + * @param linkifyMask One of [Linkify.ALL], [Linkify.PHONE_NUMBERS], [Linkify.MAP_ADDRESSES], + * [Linkify.WEB_URLS] and [Linkify.EMAIL_ADDRESSES]. + * @param textViews The TextViews on which a [BetterLinkMovementMethod] should be registered. + * @return The registered [BetterLinkMovementMethod] on the TextViews. + */ + fun linkify(linkifyMask: Int, vararg textViews: TextView): BetterLinkMovementMethod { + val movementMethod = newInstance() + for (textView in textViews) { + addLinks(linkifyMask, movementMethod, textView) + } + return movementMethod + } + + /** + * Like [.linkify], but can be used for TextViews with HTML links. + * + * @param textViews The TextViews on which a [BetterLinkMovementMethod] should be registered. + * @return The registered [BetterLinkMovementMethod] on the TextViews. + */ + fun linkifyHtml(vararg textViews: TextView): BetterLinkMovementMethod { + return linkify(LINKIFY_NONE, *textViews) + } + + /** + * Recursively register a [BetterLinkMovementMethod] on every TextView inside a layout. + * + * @param linkifyMask One of [Linkify.ALL], [Linkify.PHONE_NUMBERS], [Linkify.MAP_ADDRESSES], + * [Linkify.WEB_URLS] and [Linkify.EMAIL_ADDRESSES]. + * @return The registered [BetterLinkMovementMethod] on the TextViews. + */ + fun linkify(linkifyMask: Int, viewGroup: ViewGroup): BetterLinkMovementMethod { + val movementMethod = newInstance() + rAddLinks(linkifyMask, viewGroup, movementMethod) + return movementMethod + } + + /** + * Like [.linkify], but can be used for TextViews with HTML links. + * + * @return The registered [BetterLinkMovementMethod] on the TextViews. + */ + fun linkifyHtml(viewGroup: ViewGroup): BetterLinkMovementMethod { + return linkify(LINKIFY_NONE, viewGroup) + } + + /** + * Recursively register a [BetterLinkMovementMethod] on every TextView inside a layout. + * + * @param linkifyMask One of [Linkify.ALL], [Linkify.PHONE_NUMBERS], [Linkify.MAP_ADDRESSES], + * [Linkify.WEB_URLS] and [Linkify.EMAIL_ADDRESSES]. + * @return The registered [BetterLinkMovementMethod] on the TextViews. + */ + fun linkify(linkifyMask: Int, activity: Activity): BetterLinkMovementMethod { + // Find the layout passed to setContentView(). + val activityLayout = + (activity.findViewById(Window.ID_ANDROID_CONTENT) as ViewGroup).getChildAt(0) as ViewGroup + val movementMethod = newInstance() + rAddLinks(linkifyMask, activityLayout, movementMethod) + return movementMethod + } + + /** + * Like [.linkify], but can be used for TextViews with HTML links. + * + * @return The registered [BetterLinkMovementMethod] on the TextViews. + */ + fun linkifyHtml(activity: Activity): BetterLinkMovementMethod { + return linkify(LINKIFY_NONE, activity) + } + + /** + * Get a static instance of BetterLinkMovementMethod. Do note that registering a click listener on the returned + * instance is not supported because it will potentially be shared on multiple TextViews. + */ + val instance: BetterLinkMovementMethod? + get() { + if (singleInstance == null) { + singleInstance = BetterLinkMovementMethod() + } + return singleInstance + } + + // ======== PUBLIC APIs END ======== // + private fun rAddLinks( + linkifyMask: Int, + viewGroup: ViewGroup, + movementMethod: BetterLinkMovementMethod + ) { + for (i in 0 until viewGroup.childCount) { + val child = viewGroup.getChildAt(i) + if (child is ViewGroup) { + // Recursively find child TextViews. + rAddLinks(linkifyMask, child, movementMethod) + } else if (child is TextView) { + addLinks(linkifyMask, movementMethod, child) + } + } + } + + private fun addLinks( + linkifyMask: Int, + movementMethod: BetterLinkMovementMethod, + textView: TextView + ) { + textView.movementMethod = movementMethod + if (linkifyMask != LINKIFY_NONE) { + Linkify.addLinks(textView, linkifyMask) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt index 9644b8676..d5a376893 100644 --- a/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt @@ -48,6 +48,7 @@ open class InertiaScrollTextView @JvmOverloads constructor( mTouchSlop = vc.scaledTouchSlop mMinFlingVelocity = vc.scaledMinimumFlingVelocity mMaxFlingVelocity = vc.scaledMaximumFlingVelocity + movementMethod = BetterLinkMovementMethod.instance } fun atTop(): Boolean { diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 44fad2b64..19d45011a 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -412,8 +412,6 @@ 正文下一頁 URL 規則 (nextContentUrl) webJs 資源正則 (sourceRegex) - 网页内嵌字體(font) - 正确字体(font) 圖標 (sourceIcon) 列表規則 (ruleArticles) @@ -732,7 +730,7 @@ 恢復時忽略一些內容不恢復,方便不同手機配置不同 閱讀界面設置 图片样式(imageStyle) - 替换规则(replaceRegex) + 正文获取后处理规则(replaceRegex) 分組名稱 備註內容 默认启用替换净化 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 47622207e..fe065b1d9 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -413,8 +413,6 @@ 正文下一頁URL規則(nextContentUrl) webJs 資源正則(sourceRegex) - 网页内嵌字體(font) - 正确字体(font) 圖示(sourceIcon) 列表規則(ruleArticles) @@ -732,7 +730,7 @@ 復原時忽略一些內容不復原,方便不同手機配置不同 閱讀介面設定 圖片樣式(imageStyle) - 取代規則(replaceRegex) + 正文获取后处理规则(replaceRegex) 分組名稱 備註內容 預設啟用取代淨化 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 8c54fd559..31b6c20d4 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -418,8 +418,6 @@ 正文下一页URL规则(nextContentUrl) webJs 资源正则(sourceRegex) - 网页内嵌字体(font) - 正确字体(font) 图标(sourceIcon) 列表规则(ruleArticles) @@ -737,7 +735,7 @@ 恢复时忽略一些内容不恢复,方便不同手机配置不同 阅读界面设置 图片样式(imageStyle) - 替换规则(replaceRegex) + 正文获取后处理规则(replaceRegex) 分组名称 备注内容 默认启用替换净化 diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index cc2d9c170..c80e30395 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -5,4 +5,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5132b8358..5bf01375f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -420,10 +420,8 @@ 正文下一页URL规则(nextContentUrl) webJs 资源正则(sourceRegex) - 替换规则(replaceRegex) + 正文获取后处理规则(replaceRegex) 图片样式(imageStyle) - 网页内嵌字体(font) - 正确字体(font) 图标(sourceIcon) 列表规则(ruleArticles)