pull/497/head
gedoor 4 years ago
parent d5183123c0
commit 922b3f5943
  1. 2
      app/src/main/assets/help/appHelp.md
  2. 5
      app/src/main/assets/help/sourceHelp.md
  3. 4
      app/src/main/assets/updateLog.md
  4. 1
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  5. 7
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  6. 12
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt
  7. 437
      app/src/main/java/io/legado/app/ui/widget/text/BetterLinkMovementMethod.kt
  8. 1
      app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt
  9. 3
      app/src/main/res/values/ids.xml

@ -5,7 +5,7 @@
【温馨提醒】 *本帮助可以在我的-右上角帮助按钮再次打开,更新前一定要做好备份,以免数据丢失!* 【温馨提醒】 *本帮助可以在我的-右上角帮助按钮再次打开,更新前一定要做好备份,以免数据丢失!*
1. 为什么第一次安装好之后什么东西都没有? 1. 为什么第一次安装好之后什么东西都没有?
* 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号[开源阅读]()、QQ群、酷安评论里获取由书友制作分享的书源。 * 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号**[开源阅读]**、QQ群、酷安评论里获取由书友制作分享的书源。
2. 正文出现缺字漏字、内容缺失、排版错乱等情况,如何处理? 2. 正文出现缺字漏字、内容缺失、排版错乱等情况,如何处理?
* 有可能是净化规则出现问题,先关闭替换净化并刷新,再观察是否正常。如果正常说明净化规则存在误杀,如果关闭后仍然出现相关问题,请点击源链接查看原文与正文是否相同,如果不同,再进行反馈。 * 有可能是净化规则出现问题,先关闭替换净化并刷新,再观察是否正常。如果正常说明净化规则存在误杀,如果关闭后仍然出现相关问题,请点击源链接查看原文与正文是否相同,如果不同,再进行反馈。

@ -0,0 +1,5 @@
# 源规则帮助
* [书源帮助文档](https://alanskycn.gitee.io/teachme/Rule/source.html)
* [订阅源帮助文档](https://alanskycn.gitee.io/teachme/Rule/rss.html)

@ -1,6 +1,6 @@
# 更新日志 # 更新日志
* 关注公众号 **[开源阅读]()** 菜单•软件下载 提前享受新版本。 * 关注公众号 **[开源阅读]** 菜单•软件下载 提前享受新版本。
* 关注合作公众号 **[小说拾遗]()** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/11/18** **2020/11/18**

@ -13,7 +13,6 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.analyzeRule.QueryTTF import io.legado.app.model.analyzeRule.QueryTTF
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.htmlFormat import io.legado.app.utils.htmlFormat
import io.legado.app.utils.toStringArray
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext

@ -390,13 +390,18 @@ class BookSourceEditActivity :
selector(getString(R.string.help), items) { _, index -> selector(getString(R.string.help), items) { _, index ->
when (index) { when (index) {
0 -> insertText(AppConst.urlOption) 0 -> insertText(AppConst.urlOption)
1 -> openUrl("https://alanskycn.gitee.io/teachme/Rule/source.html") 1 -> showSourceHelp()
2 -> showRegexHelp() 2 -> showRegexHelp()
3 -> FilePicker.selectFile(this, selectPathRequestCode) 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() { private fun showRegexHelp() {
val mdText = String(assets.open("help/regexHelp.md").readBytes()) val mdText = String(assets.open("help/regexHelp.md").readBytes())
TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) TextDialog.show(supportFragmentManager, mdText, TextDialog.MD)

@ -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.rss.source.debug.RssSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.ui.widget.dialog.TextDialog 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 kotlinx.android.synthetic.main.activity_rss_source_edit.*
import org.jetbrains.anko.* import org.jetbrains.anko.*
import kotlin.math.abs import kotlin.math.abs
@ -199,12 +202,17 @@ class RssSourceEditActivity :
selector(getString(R.string.help), items) { _, index -> selector(getString(R.string.help), items) { _, index ->
when (index) { when (index) {
0 -> insertText(AppConst.urlOption) 0 -> insertText(AppConst.urlOption)
1 -> openUrl("https://alanskycn.gitee.io/teachme/Rule/rss.html") 1 -> showSourceHelp()
2 -> showRegexHelp() 2 -> showRegexHelp()
} }
} }
} }
private fun showSourceHelp() {
val mdText = String(assets.open("help/sourceHelp.md").readBytes())
TextDialog.show(supportFragmentManager, mdText, TextDialog.MD)
}
private fun showRegexHelp() { private fun showRegexHelp() {
val mdText = String(assets.open("help/regexHelp.md").readBytes()) val mdText = String(assets.open("help/regexHelp.md").readBytes())
TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) TextDialog.show(supportFragmentManager, mdText, TextDialog.MD)

@ -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 <var>clickableSpan</var>'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<View>(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)
}
}
}
}

@ -48,6 +48,7 @@ open class InertiaScrollTextView @JvmOverloads constructor(
mTouchSlop = vc.scaledTouchSlop mTouchSlop = vc.scaledTouchSlop
mMinFlingVelocity = vc.scaledMinimumFlingVelocity mMinFlingVelocity = vc.scaledMinimumFlingVelocity
mMaxFlingVelocity = vc.scaledMaximumFlingVelocity mMaxFlingVelocity = vc.scaledMaximumFlingVelocity
movementMethod = BetterLinkMovementMethod.instance
} }
fun atTop(): Boolean { fun atTop(): Boolean {

@ -5,4 +5,7 @@
<item name="tag2" type="id" /> <item name="tag2" type="id" />
<item name="fast_scroller" type="id" /> <item name="fast_scroller" type="id" />
<item name="bettermovementmethod_highlight_background_span" type="id" />
</resources> </resources>
Loading…
Cancel
Save