pull/395/head
Celeter 4 years ago
commit be1fdbdbba
  1. 23
      app/src/main/assets/updateLog.md
  2. 35
      app/src/main/java/io/legado/app/api/ReaderProvider.kt
  3. 4
      app/src/main/java/io/legado/app/constant/EventBus.kt
  4. 2
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  5. 3
      app/src/main/java/io/legado/app/help/AppConfig.kt
  6. 2
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  7. 1
      app/src/main/java/io/legado/app/help/ReadTipConfig.kt
  8. 1
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  9. 25
      app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt
  10. 134
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  11. 35
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  12. 14
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  13. 1
      app/src/main/java/io/legado/app/ui/README.md
  14. 26
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceActivity.kt
  15. 24
      app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt
  16. 6
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt
  17. 15
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  18. 6
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowAdapter.kt
  19. 35
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  20. 17
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  21. 19
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  22. 19
      app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt
  23. 271
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListActivity.kt
  24. 19
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListAdapter.kt
  25. 239
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListFragment.kt
  26. 10
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListViewModel.kt
  27. 32
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt
  28. 72
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  29. 11
      app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt
  30. 22
      app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt
  31. 83
      app/src/main/res/layout/activity_search_list.xml
  32. 36
      app/src/main/res/layout/dialog_progressbar_view.xml
  33. 66
      app/src/main/res/layout/fragment_search_list.xml
  34. 58
      app/src/main/res/layout/view_read_menu.xml
  35. 6
      app/src/main/res/menu/change_source.xml
  36. 4
      app/src/main/res/menu/content_select_action.xml
  37. 6
      app/src/main/res/menu/import_source.xml
  38. 11
      app/src/main/res/values-zh-rHK/arrays.xml
  39. 10
      app/src/main/res/values-zh-rHK/strings.xml
  40. 1
      app/src/main/res/values-zh-rTW/arrays.xml
  41. 11
      app/src/main/res/values-zh-rTW/strings.xml
  42. 1
      app/src/main/res/values-zh/arrays.xml
  43. 13
      app/src/main/res/values-zh/strings.xml
  44. 1
      app/src/main/res/values/arrays.xml
  45. 1
      app/src/main/res/values/pref_key_value.xml
  46. 13
      app/src/main/res/values/strings.xml
  47. 9
      app/src/main/res/xml/pref_config_backup.xml
  48. 7
      app/src/main/res/xml/pref_config_other.xml

@ -3,6 +3,29 @@
* 关注合作公众号 **[小说拾遗]()** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]()** 获取好看的小说。
- 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 - 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/09/23**
* 修复规则解析bug
**2020/09/21**
* 修复规则解析bug
* 换源时无最新章节信息时可加载详情页来获取(默认关闭)
**2020/09/20**
* 优化正文搜索
* 阅读界面信息添加书名
**2020/09/18**
* 解决正文替换{{title}}问题
* 修复共用布局配置不能读取的问题
* 添加自定义源分组功能 by KKL369
* 解决跨进程调用ReaderProvider出现CursorIndexOutOfBoundsException问题
**2020/09/17**
* 优化正文搜索文字颜色
* 优化书源校验 by KKL369
* 缓存导出到webDav by 10bits
* 导入的字体在字体选择界面显示
**2020/09/15** **2020/09/15**
* 修复导入排版字体重复报错的bug * 修复导入排版字体重复报错的bug
* 添加正文搜索 by [h11128](https://github.com/h11128) * 添加正文搜索 by [h11128](https://github.com/h11128)

@ -7,10 +7,7 @@ import android.content.ContentProvider
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentValues import android.content.ContentValues
import android.content.UriMatcher import android.content.UriMatcher
import android.database.CharArrayBuffer import android.database.*
import android.database.ContentObserver
import android.database.Cursor
import android.database.DataSetObserver
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import com.google.gson.Gson import com.google.gson.Gson
@ -109,40 +106,22 @@ class ReaderProvider : ContentProvider() {
uri: Uri, values: ContentValues?, selection: String?, uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>? selectionArgs: Array<String>?
) = throw UnsupportedOperationException("Not yet implemented") ) = throw UnsupportedOperationException("Not yet implemented")
/** /**
* Simple inner class to deliver json callback data. * Simple inner class to deliver json callback data.
* *
* Only getString() makes sense. * Only getString() makes sense.
*/ */
private class SimpleCursor(data: ReturnData?) : Cursor { private class SimpleCursor(data: ReturnData?) : MatrixCursor(arrayOf("result"), 1) {
private val mData: String = Gson().toJson(data) private val mData: String = Gson().toJson(data)
override fun getCount() = 1 init {
addRow(arrayOf(mData))
override fun getPosition() = 0 }
override fun move(i: Int) = true
override fun moveToPosition(i: Int) = true
override fun moveToFirst() = true
override fun moveToLast() = true
override fun moveToNext() = true
override fun moveToPrevious() = true
override fun isFirst() = true
override fun isLast() = true
override fun isBeforeFirst() = true
override fun isAfterLast() = true override fun getCount() = 1
override fun getColumnIndex(s: String) = 0 override fun getColumnIndex(s: String) = 0

@ -20,4 +20,8 @@ object EventBus {
const val WEB_SERVICE_STOP = "webServiceStop" const val WEB_SERVICE_STOP = "webServiceStop"
const val UP_DOWNLOAD = "upDownload" const val UP_DOWNLOAD = "upDownload"
const val SAVE_CONTENT = "saveContent" const val SAVE_CONTENT = "saveContent"
const val CHECK_INIT = "checkInit"
const val CHECK_UP_PROGRESS = "checkProgress"
const val CHECK_UP_PROGRESS_STRING = "checkProgressTxt"
const val CHECK_DONE = "checkDone"
} }

@ -33,7 +33,9 @@ object PreferKey {
const val webDavAccount = "web_dav_account" const val webDavAccount = "web_dav_account"
const val webDavPassword = "web_dav_password" const val webDavPassword = "web_dav_password"
const val webDavCreateDir = "webDavCreateDir" const val webDavCreateDir = "webDavCreateDir"
const val webDavCacheBackup = "webDavCacheBackup"
const val changeSourceLoadToc = "changeSourceLoadToc" const val changeSourceLoadToc = "changeSourceLoadToc"
const val changeSourceLoadInfo = "changeSourceLoadInfo"
const val chineseConverterType = "chineseConverterType" const val chineseConverterType = "chineseConverterType"
const val launcherIcon = "launcherIcon" const val launcherIcon = "launcherIcon"
const val textSelectAble = "selectText" const val textSelectAble = "selectText"

@ -58,6 +58,9 @@ object AppConfig {
App.INSTANCE.putPrefBoolean(PreferKey.showRss, value) App.INSTANCE.putPrefBoolean(PreferKey.showRss, value)
} }
val backgroundVerification: Boolean
get() = App.INSTANCE.getPrefBoolean(R.string.pk_background_verification)
val autoRefreshBook: Boolean val autoRefreshBook: Boolean
get() = App.INSTANCE.getPrefBoolean(R.string.pk_auto_refresh) get() = App.INSTANCE.getPrefBoolean(R.string.pk_auto_refresh)

@ -75,7 +75,7 @@ object ReadBookConfig {
} }
fun initShareConfig() { fun initShareConfig() {
val configFile = File(configFilePath) val configFile = File(shareConfigFilePath)
var c: Config? = null var c: Config? = null
if (configFile.exists()) { if (configFile.exists()) {
try { try {

@ -12,6 +12,7 @@ object ReadTipConfig {
const val page = 4 const val page = 4
const val totalProgress = 5 const val totalProgress = 5
const val pageAndTotal = 6 const val pageAndTotal = 6
const val bookName = 7
val tipHeaderLeftStr: String get() = tipArray.getOrElse(tipHeaderLeft) { tipArray[none] } val tipHeaderLeftStr: String get() = tipArray.getOrElse(tipHeaderLeft) { tipArray[none] }
val tipHeaderMiddleStr: String get() = tipArray.getOrElse(tipHeaderMiddle) { tipArray[none] } val tipHeaderMiddleStr: String get() = tipArray.getOrElse(tipHeaderMiddle) { tipArray[none] }

@ -54,6 +54,7 @@ object Backup {
context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis()) context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
withContext(IO) { withContext(IO) {
synchronized(this@Backup) { synchronized(this@Backup) {
FileUtils.deleteFile(backupPath)
writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath) writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookmarkDao().all, "bookmark.json", backupPath) writeListToJson(App.db.bookmarkDao().all, "bookmark.json", backupPath)
writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath) writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath)

@ -10,10 +10,7 @@ import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.webdav.WebDav import io.legado.app.lib.webdav.WebDav
import io.legado.app.lib.webdav.http.HttpAuth import io.legado.app.lib.webdav.http.HttpAuth
import io.legado.app.utils.FileUtils import io.legado.app.utils.*
import io.legado.app.utils.ZipUtils
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -125,4 +122,24 @@ object WebDavHelp {
} }
} }
} }
fun exportWebDav(path: String, fileName: String) {
try {
if (initWebDav()) {
// 默认导出到legado文件夹下exports目录
val exportsWebDavUrl = rootWebDavUrl + EncoderUtils.escape("exports") + "/"
// 在legado文件夹创建exports目录,如果不存在的话
WebDav(exportsWebDavUrl).makeAsDir()
val file = File("${path}${File.separator}${fileName}")
// 如果导出的本地文件存在,开始上传
if(file.exists()) {
val putUrl = exportsWebDavUrl + fileName
WebDav(putUrl).upload("${path}${File.separator}${fileName}")
}
}
} catch (e: Exception) {
Handler(Looper.getMainLooper()).post {
App.INSTANCE.toast("WebDav导出\n${e.localizedMessage}")
}
}
}
} }

@ -238,6 +238,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
if (ruleList.isNotEmpty()) result = o if (ruleList.isNotEmpty()) result = o
for (sourceRule in ruleList) { for (sourceRule in ruleList) {
putRule(sourceRule.putMap) putRule(sourceRule.putMap)
sourceRule.makeUpRule(result)
result?.let { result?.let {
result = when (sourceRule.mode) { result = when (sourceRule.mode) {
Mode.Regex -> AnalyzeByRegex.getElement( Mode.Regex -> AnalyzeByRegex.getElement(
@ -324,37 +325,17 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
*/ */
private fun replaceRegex(result: String, rule: SourceRule): String { private fun replaceRegex(result: String, rule: SourceRule): String {
var vResult = result var vResult = result
val stringBuffer = StringBuffer() if (rule.replaceRegex.isNotEmpty()) {
val evalMatcher = replacePattern.matcher(rule.replaceRegex)
while (evalMatcher.find()) {
val jsEval = evalMatcher.group().let {
if (it.startsWith("@get:", true)) {
get(it.substring(6, it.lastIndex))
} else {
evalJS(it.substring(2, it.length - 2), result)
}
} ?: ""
if (jsEval is String) {
evalMatcher.appendReplacement(stringBuffer, jsEval)
} else if (jsEval is Double && jsEval % 1.0 == 0.0) {
evalMatcher.appendReplacement(stringBuffer, String.format("%.0f", jsEval))
} else {
evalMatcher.appendReplacement(stringBuffer, jsEval.toString())
}
}
evalMatcher.appendTail(stringBuffer)
val replaceRegex = stringBuffer.toString()
if (replaceRegex.isNotEmpty()) {
vResult = if (rule.replaceFirst) { vResult = if (rule.replaceFirst) {
val pattern = Pattern.compile(replaceRegex) val pattern = Pattern.compile(rule.replaceRegex)
val matcher = pattern.matcher(vResult) val matcher = pattern.matcher(vResult)
if (matcher.find()) { if (matcher.find()) {
matcher.group(0)!!.replaceFirst(replaceRegex.toRegex(), rule.replacement) matcher.group(0)!!.replaceFirst(rule.replaceRegex.toRegex(), rule.replacement)
} else { } else {
"" ""
} }
} else { } else {
vResult.replace(replaceRegex.toRegex(), rule.replacement) vResult.replace(rule.replaceRegex.toRegex(), rule.replacement)
} }
} }
return vResult return vResult
@ -425,6 +406,9 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
internal val putMap = HashMap<String, String>() internal val putMap = HashMap<String, String>()
private val ruleParam = ArrayList<String>() private val ruleParam = ArrayList<String>()
private val ruleType = ArrayList<Int>() private val ruleType = ArrayList<Int>()
private val getRuleType = -2
private val jsRuleType = -1
private val defaultRuleType = 0
init { init {
this.mode = mainMode this.mode = mainMode
@ -465,68 +449,71 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
} }
//分离put //分离put
rule = splitPutRule(rule, putMap) rule = splitPutRule(rule, putMap)
//分离正则表达式 //@get,{{ }}, 拆分
val index = rule.indexOf("}}")
var rule1 = ""
var rule2 = rule
if (index > 0) {
rule1 = rule.substring(0, index)
rule2 = rule.substring(index)
}
val ruleStrS = rule2.trim { it <= ' ' }.split("##")
rule = rule1 + ruleStrS[0]
if (ruleStrS.size > 1) {
replaceRegex = ruleStrS[1]
}
if (ruleStrS.size > 2) {
replacement = ruleStrS[2]
}
if (ruleStrS.size > 3) {
replaceFirst = true
}
//@get,{{ }},$1, 拆分
var start = 0 var start = 0
var tmp: String var tmp: String
val evalMatcher = evalPattern.matcher(rule) val evalMatcher = evalPattern.matcher(rule)
while (evalMatcher.find()) { while (evalMatcher.find()) {
if (mode != Mode.Js) {
mode = Mode.Regex
}
if (evalMatcher.start() > start) { if (evalMatcher.start() > start) {
tmp = rule.substring(start, evalMatcher.start()) tmp = rule.substring(start, evalMatcher.start())
ruleType.add(0) if (start == 0 && !tmp.contains("##") && mode != Mode.Js) {
ruleParam.add(tmp) mode = Mode.Regex
}
splitRegex(tmp)
} }
tmp = evalMatcher.group() tmp = evalMatcher.group()
when { when {
tmp.startsWith("$") -> {
ruleType.add(tmp.substring(1).toInt())
ruleParam.add(tmp)
}
tmp.startsWith("@get:", true) -> { tmp.startsWith("@get:", true) -> {
ruleType.add(-2) ruleType.add(getRuleType)
ruleParam.add(tmp.substring(6, tmp.lastIndex)) ruleParam.add(tmp.substring(6, tmp.lastIndex))
} }
tmp.startsWith("{{") -> { tmp.startsWith("{{") -> {
ruleType.add(-1) ruleType.add(jsRuleType)
ruleParam.add(tmp.substring(2, tmp.length - 2)) ruleParam.add(tmp.substring(2, tmp.length - 2))
} }
else -> { else -> {
ruleType.add(0) splitRegex(tmp)
ruleParam.add(tmp)
} }
} }
start = evalMatcher.end() start = evalMatcher.end()
} }
if (rule.length > start) { if (rule.length > start) {
tmp = rule.substring(start) tmp = rule.substring(start)
ruleType.add(0) splitRegex(tmp)
}
}
/**
* 拆分\$\d{1,2}
*/
private fun splitRegex(ruleStr: String) {
var start = 0
var tmp: String
val ruleStrArray = ruleStr.split("##")
val regexMatcher = regexPattern.matcher(ruleStrArray[0])
while (regexMatcher.find()) {
if (mode != Mode.Js) {
mode = Mode.Regex
}
if (regexMatcher.start() > start) {
tmp = ruleStr.substring(start, regexMatcher.start())
ruleType.add(defaultRuleType)
ruleParam.add(tmp)
}
tmp = regexMatcher.group()
ruleType.add(tmp.substring(1).toInt())
ruleParam.add(tmp)
start = regexMatcher.end()
}
if (ruleStr.length > start) {
tmp = ruleStr.substring(start)
ruleType.add(defaultRuleType)
ruleParam.add(tmp) ruleParam.add(tmp)
} }
} }
/** /**
* 替换@get,{{ }},$1, * 替换@get,{{ }}
*/ */
fun makeUpRule(result: Any?) { fun makeUpRule(result: Any?) {
val infoVal = StringBuilder() val infoVal = StringBuilder()
@ -535,7 +522,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
while (index-- > 0) { while (index-- > 0) {
val regType = ruleType[index] val regType = ruleType[index]
when { when {
regType > 0 -> { regType > defaultRuleType -> {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val resultList = result as? List<String?> val resultList = result as? List<String?>
if (resultList != null) { if (resultList != null) {
@ -548,7 +535,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
infoVal.insert(0, ruleParam[index]) infoVal.insert(0, ruleParam[index])
} }
} }
regType == -1 -> { regType == jsRuleType -> {
if (isRule(ruleParam[index])) { if (isRule(ruleParam[index])) {
getString(arrayListOf(SourceRule(ruleParam[index]))).let { getString(arrayListOf(SourceRule(ruleParam[index]))).let {
infoVal.insert(0, it) infoVal.insert(0, it)
@ -566,7 +553,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
} }
} }
} }
regType == -2 -> { regType == getRuleType -> {
infoVal.insert(0, get(ruleParam[index])) infoVal.insert(0, get(ruleParam[index]))
} }
else -> infoVal.insert(0, ruleParam[index]) else -> infoVal.insert(0, ruleParam[index])
@ -574,6 +561,18 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
} }
rule = infoVal.toString() rule = infoVal.toString()
} }
//分离正则表达式
val ruleStrS = rule.trim { it <= ' ' }.split("##")
rule = ruleStrS[0]
if (ruleStrS.size > 1) {
replaceRegex = ruleStrS[1]
}
if (ruleStrS.size > 2) {
replacement = ruleStrS[2]
}
if (ruleStrS.size > 3) {
replaceFirst = true
}
} }
private fun isRule(ruleStr: String): Boolean { private fun isRule(ruleStr: String): Boolean {
@ -660,14 +659,9 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
companion object { companion object {
private val putPattern = Pattern.compile("@put:(\\{[^}]+?\\})", Pattern.CASE_INSENSITIVE) private val putPattern = Pattern.compile("@put:(\\{[^}]+?\\})", Pattern.CASE_INSENSITIVE)
private val getPattern = Pattern.compile("@get:\\{([^}]+?)\\}", Pattern.CASE_INSENSITIVE) private val getPattern = Pattern.compile("@get:\\{([^}]+?)\\}", Pattern.CASE_INSENSITIVE)
private val evalPattern = Pattern.compile( private val evalPattern =
"@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}|\\$\\d{1,2}", Pattern.compile("@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}", Pattern.CASE_INSENSITIVE)
Pattern.CASE_INSENSITIVE private val regexPattern = Pattern.compile("\\$\\d{1,2}")
)
private val replacePattern = Pattern.compile(
"@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}",
Pattern.CASE_INSENSITIVE
)
} }
} }

@ -6,12 +6,15 @@ import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.AppConfig.backgroundVerification
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.service.help.CheckSource import io.legado.app.service.help.CheckSource
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.utils.postEvent
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -42,7 +45,9 @@ class CheckSourceService : BaseService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
updateNotification(0, getString(R.string.start)) if (backgroundVerification) {
updateNotification(0, getString(R.string.start))
}
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -59,6 +64,7 @@ class CheckSourceService : BaseService() {
super.onDestroy() super.onDestroy()
tasks.clear() tasks.clear()
searchPool.close() searchPool.close()
postEvent(EventBus.CHECK_DONE, 0)
} }
private fun check(ids: List<String>) { private fun check(ids: List<String>) {
@ -72,7 +78,11 @@ class CheckSourceService : BaseService() {
allIds.addAll(ids) allIds.addAll(ids)
processIndex = 0 processIndex = 0
threadCount = min(allIds.size, threadCount) threadCount = min(allIds.size, threadCount)
updateNotification(0, getString(R.string.progress_show, 0, allIds.size)) if (backgroundVerification) {
updateNotification(0, getString(R.string.progress_show, "", 0, allIds.size))
} else {
postEvent(EventBus.CHECK_INIT, allIds.size)
}
for (i in 0 until threadCount) { for (i in 0 until threadCount) {
check() check()
} }
@ -91,25 +101,30 @@ class CheckSourceService : BaseService() {
val sourceUrl = allIds[index] val sourceUrl = allIds[index]
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source -> App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
if (source.searchUrl.isNullOrEmpty()) { if (source.searchUrl.isNullOrEmpty()) {
onNext(sourceUrl) onNext(sourceUrl, source.bookSourceName)
} else { } else {
CheckSource(source).check(this, searchPool) { CheckSource(source).check(this, searchPool) {
onNext(it) onNext(it, source.bookSourceName)
} }
} }
} ?: onNext(sourceUrl) } ?: onNext(sourceUrl, "")
} }
} }
} }
private fun onNext(sourceUrl: String) { private fun onNext(sourceUrl: String, sourceName: String) {
synchronized(this) { synchronized(this) {
check() check()
checkedIds.add(sourceUrl) checkedIds.add(sourceUrl)
updateNotification( if (backgroundVerification) {
checkedIds.size, updateNotification(
getString(R.string.progress_show, checkedIds.size, allIds.size) checkedIds.size,
) getString(R.string.progress_show, sourceName, checkedIds.size, allIds.size)
)
} else {
postEvent(EventBus.CHECK_UP_PROGRESS, checkedIds.size)
postEvent(EventBus.CHECK_UP_PROGRESS_STRING, getString(R.string.progress_show, sourceName, checkedIds.size, allIds.size))
}
if (processIndex >= allIds.size + threadCount - 1) { if (processIndex >= allIds.size + threadCount - 1) {
stopSelf() stopSelf()
} }

@ -368,7 +368,19 @@ object ReadBook {
val currentLine = currentPage.textLines[lineIndex] val currentLine = currentPage.textLines[lineIndex]
length -= currentLine.text.length length -= currentLine.text.length
val charIndex = contentPosition - length val charIndex = contentPosition - length
return arrayOf(pageIndex, lineIndex, charIndex) var addLine = 0
var charIndex2 = 0
// change line
if ((charIndex + query.length) > currentLine.text.length){
addLine = 1
charIndex2 = charIndex + query.length - currentLine.text.length - 1
}
// changePage
if ((lineIndex + addLine + 1) > currentPage.textLines.size){
addLine = -1
charIndex2 = charIndex + query.length - currentLine.text.length - 1
}
return arrayOf(pageIndex, lineIndex, charIndex, addLine, charIndex2)
} }
/** /**

@ -1,6 +1,7 @@
## 放置与界面有关的类 ## 放置与界面有关的类
* about 关于界面 * about 关于界面
* association 导入书源界面
* audio 音频播放界面 * audio 音频播放界面
* book\arrange 书架整理界面 * book\arrange 书架整理界面
* book\info 书籍信息查看 * book\info 书籍信息查看

@ -20,16 +20,21 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentDataHelp
import io.legado.app.help.SourceHelp import io.legado.app.help.SourceHelp
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton import io.legado.app.lib.dialogs.okButton
import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.visible import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.activity_translucence.* import kotlinx.android.synthetic.main.activity_translucence.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.* import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_source_import.view.* import kotlinx.android.synthetic.main.item_source_import.view.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
class ImportBookSourceActivity : VMBaseActivity<ImportBookSourceViewModel>( class ImportBookSourceActivity : VMBaseActivity<ImportBookSourceViewModel>(
R.layout.activity_translucence, R.layout.activity_translucence,
theme = Theme.Transparent theme = Theme.Transparent
@ -112,6 +117,7 @@ class ImportBookSourceActivity : VMBaseActivity<ImportBookSourceViewModel>(
class SourcesDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { class SourcesDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
lateinit var adapter: SourcesAdapter lateinit var adapter: SourcesAdapter
private var _groupName: String? = null
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -165,6 +171,23 @@ class ImportBookSourceActivity : VMBaseActivity<ImportBookSourceViewModel>(
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_new_group -> {
alert(R.string.diy_edit_source_group) {
var editText: AutoCompleteTextView? = null
customView {
layoutInflater.inflate(R.layout.dialog_edit_text, null).apply {
editText = edit_view
}
}
okButton {
editText?.text?.toString()?.let { group ->
_groupName = group
item.title = getString(R.string.diy_edit_source_group_title, _groupName)
}
}
noButton { }
}.show().applyTint()
}
R.id.menu_select_all -> { R.id.menu_select_all -> {
adapter.selectStatus.forEachIndexed { index, b -> adapter.selectStatus.forEachIndexed { index, b ->
if (!b) { if (!b) {
@ -193,6 +216,9 @@ class ImportBookSourceActivity : VMBaseActivity<ImportBookSourceViewModel>(
private fun importSelect() { private fun importSelect() {
val selectSource = arrayListOf<BookSource>() val selectSource = arrayListOf<BookSource>()
adapter.selectStatus.forEachIndexed { index, b -> adapter.selectStatus.forEachIndexed { index, b ->
if (_groupName != null) {
adapter.getItem(index)!!.bookSourceGroup = _groupName
}
if (b) { if (b) {
selectSource.add(adapter.getItem(index)!!) selectSource.add(adapter.getItem(index)!!)
} }

@ -7,8 +7,10 @@ import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.storage.WebDavHelp
import io.legado.app.utils.* import io.legado.app.utils.*
import java.io.File import java.io.File
@ -34,8 +36,20 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
} }
private fun export(doc: DocumentFile, book: Book) { private fun export(doc: DocumentFile, book: Book) {
DocumentUtils.createFileIfNotExist(doc, "${book.name} 作者:${book.author}.txt") val filename = "${book.name} by ${book.author}.txt"
?.writeText(context, getAllContents(book)) val content = getAllContents(book)
DocumentUtils.createFileIfNotExist(doc, filename)
?.writeText(context, content)
if(App.INSTANCE.getPrefBoolean(PreferKey.webDavCacheBackup,false)) {
FileUtils.createFileIfNotExist(
File(FileUtils.getCachePath()),
filename
).writeText(content) // 写出文件到cache目录
// 导出到webdav
WebDavHelp.exportWebDav(FileUtils.getCachePath(), filename)
// 上传完删除cache文件
FileUtils.deleteFile("${FileUtils.getCachePath()}${File.separator}${filename}")
}
App.db.bookChapterDao().getChapterList(book.bookUrl).forEach { chapter -> App.db.bookChapterDao().getChapterList(book.bookUrl).forEach { chapter ->
BookHelp.getContent(book, chapter).let { content -> BookHelp.getContent(book, chapter).let { content ->
content?.split("\n")?.forEachIndexed { index, text -> content?.split("\n")?.forEachIndexed { index, text ->
@ -61,8 +75,12 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
} }
private fun export(file: File, book: Book) { private fun export(file: File, book: Book) {
FileUtils.createFileIfNotExist(file, "${book.name} 作者:${book.author}.txt") val filename = "${book.name} by ${book.author}.txt"
FileUtils.createFileIfNotExist(file, filename)
.writeText(getAllContents(book)) .writeText(getAllContents(book))
if(App.INSTANCE.getPrefBoolean(PreferKey.webDavCacheBackup,false)) {
WebDavHelp.exportWebDav(file.absolutePath, filename) // 导出到webdav
}
App.db.bookChapterDao().getChapterList(book.bookUrl).forEach { chapter -> App.db.bookChapterDao().getChapterList(book.bookUrl).forEach { chapter ->
BookHelp.getContent(book, chapter).let { content -> BookHelp.getContent(book, chapter).let { content ->
content?.split("\n")?.forEachIndexed { index, text -> content?.split("\n")?.forEachIndexed { index, text ->

@ -88,6 +88,8 @@ class ChangeSourceDialog : BaseDialogFragment(),
tool_bar.setOnMenuItemClickListener(this) tool_bar.setOnMenuItemClickListener(this)
tool_bar.menu.findItem(R.id.menu_load_toc)?.isChecked = tool_bar.menu.findItem(R.id.menu_load_toc)?.isChecked =
getPrefBoolean(PreferKey.changeSourceLoadToc) getPrefBoolean(PreferKey.changeSourceLoadToc)
tool_bar.menu.findItem(R.id.menu_load_info)?.isChecked =
getPrefBoolean(PreferKey.changeSourceLoadInfo)
} }
private fun initRecyclerView() { private fun initRecyclerView() {
@ -159,6 +161,10 @@ class ChangeSourceDialog : BaseDialogFragment(),
putPrefBoolean(PreferKey.changeSourceLoadToc, !item.isChecked) putPrefBoolean(PreferKey.changeSourceLoadToc, !item.isChecked)
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
} }
R.id.menu_load_info -> {
putPrefBoolean(PreferKey.changeSourceLoadInfo, !item.isChecked)
item.isChecked = !item.isChecked
}
R.id.menu_stop -> viewModel.stopSearch() R.id.menu_stop -> viewModel.stopSearch()
} }
return false return false

@ -120,11 +120,11 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
.onSuccess(IO) { .onSuccess(IO) {
it.forEach { searchBook -> it.forEach { searchBook ->
if (searchBook.name == name && searchBook.author == author) { if (searchBook.name == name && searchBook.author == author) {
if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) { if (searchBook.latestChapterTitle.isNullOrEmpty()) {
if (searchBook.tocUrl.isEmpty()) { if (context.getPrefBoolean(PreferKey.changeSourceLoadInfo) || context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
loadBookInfo(searchBook.toBook()) loadBookInfo(searchBook.toBook())
} else { } else {
loadChapter(searchBook.toBook()) searchFinish(searchBook)
} }
} else { } else {
searchFinish(searchBook) searchFinish(searchBook)
@ -156,7 +156,14 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this) WebBook(bookSource).getBookInfo(book, this)
.onSuccess { .onSuccess {
loadChapter(it) if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
loadChapter(it)
} else {
//从详情页里获取最新章节
book.latestChapterTitle = it.latestChapterTitle
val searchBook = book.toSearchBook()
searchFinish(searchBook)
}
}.onError { }.onError {
debug { context.getString(R.string.error_get_book_info) } debug { context.getString(R.string.error_get_book_info) }
} }

@ -26,7 +26,11 @@ class ExploreShowAdapter(context: Context, val callBack: CallBack) :
tv_lasted.text = context.getString(R.string.lasted_show, item.latestChapterTitle) tv_lasted.text = context.getString(R.string.lasted_show, item.latestChapterTitle)
tv_lasted.visible() tv_lasted.visible()
} }
tv_introduce.text = context.getString(R.string.intro_show, item.intro) if (item.intro.isNullOrEmpty()) {
tv_introduce.text = context.getString(R.string.intro_show_null)
} else {
tv_introduce.text = context.getString(R.string.intro_show, item.intro)
}
val kinds = item.getKindList() val kinds = item.getKindList()
if (kinds.isEmpty()) { if (kinds.isEmpty()) {
ll_kind.gone() ll_kind.gone()

@ -49,7 +49,6 @@ 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.TextPageFactory
import io.legado.app.ui.book.read.page.delegate.PageDelegate import io.legado.app.ui.book.read.page.delegate.PageDelegate
import io.legado.app.ui.book.searchContent.SearchListActivity import io.legado.app.ui.book.searchContent.SearchListActivity
import io.legado.app.ui.book.searchContent.SearchResult
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.login.SourceLogin import io.legado.app.ui.login.SourceLogin
import io.legado.app.ui.replacerule.ReplaceRuleActivity import io.legado.app.ui.replacerule.ReplaceRuleActivity
@ -58,16 +57,14 @@ import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_book_read.* import kotlinx.android.synthetic.main.activity_book_read.*
import kotlinx.android.synthetic.main.view_read_menu.* import kotlinx.android.synthetic.main.view_read_menu.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import java.lang.Runnable
class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_book_read), class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_book_read),
View.OnTouchListener, View.OnTouchListener,
@ -496,6 +493,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
) )
return true return true
} }
R.id.menu_search_content -> {
openSearchActivity(selectedText)
return true
}
} }
return false return false
} }
@ -680,12 +681,12 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
/** /**
* 打开搜索界面 * 打开搜索界面
*/ */
//todo: change request code override fun openSearchActivity(searchWord: String?) {
override fun openSearchList() {
ReadBook.book?.let { ReadBook.book?.let {
startActivityForResult<SearchListActivity>( startActivityForResult<SearchListActivity>(
requestCodeSearchResult, requestCodeSearchResult,
Pair("bookUrl", it.bookUrl) Pair("bookUrl", it.bookUrl),
Pair("searchWord", searchWord)
) )
} }
} }
@ -787,14 +788,22 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
pages = ReadBook.curTextChapter?.pages pages = ReadBook.curTextChapter?.pages
} }
val positions = ReadBook.searchResultPositions(pages, indexWithinChapter, query!!) val positions = ReadBook.searchResultPositions(pages, indexWithinChapter, query!!)
//todo: show selected text while (ReadBook.durPageIndex != positions[0]){
val job1 = async(Main){ delay(100L)
ReadBook.skipToPage(positions[0]) ReadBook.skipToPage(positions[0])
page_view.curPage.selectStartMoveIndex(positions[0], positions[1], 0) }
page_view.curPage.selectEndMoveIndex(positions[0], positions[1], 0 + query.length ) withContext(Main){
page_view.curPage.selectStartMoveIndex(0, positions[1], positions[2])
delay(20L)
when (positions[3]){
0 -> page_view.curPage.selectEndMoveIndex(0, positions[1], positions[2] + query.length - 1)
1 -> page_view.curPage.selectEndMoveIndex(0, positions[1] + 1, positions[4])
//todo: consider change page, jump to scroll position
-1 -> page_view.curPage.selectEndMoveIndex(1, 0, positions[4])
}
page_view.isTextSelected = true page_view.isTextSelected = true
delay(100L)
} }
job1.await()
} }
} }
requestCodeReplace -> onReplaceRuleSave() requestCodeReplace -> onReplaceRuleSave()

@ -65,6 +65,8 @@ class ReadMenu : FrameLayout {
brightnessBackground.setColor(ColorUtils.adjustAlpha(bgColor, 0.5f)) brightnessBackground.setColor(ColorUtils.adjustAlpha(bgColor, 0.5f))
ll_brightness.background = brightnessBackground ll_brightness.background = brightnessBackground
ll_bottom_bg.setBackgroundColor(bgColor) ll_bottom_bg.setBackgroundColor(bgColor)
fabSearch.backgroundTintList = bottomBackgroundList
fabSearch.setColorFilter(textColor)
fabAutoPage.backgroundTintList = bottomBackgroundList fabAutoPage.backgroundTintList = bottomBackgroundList
fabAutoPage.setColorFilter(textColor) fabAutoPage.setColorFilter(textColor)
fabReplaceRule.backgroundTintList = bottomBackgroundList fabReplaceRule.backgroundTintList = bottomBackgroundList
@ -170,6 +172,13 @@ class ReadMenu : FrameLayout {
} }
}) })
//搜索
fabSearch.onClick {
runMenuOut {
callBack?.openSearchActivity(null)
}
}
//自动翻页 //自动翻页
fabAutoPage.onClick { fabAutoPage.onClick {
runMenuOut { runMenuOut {
@ -200,12 +209,6 @@ class ReadMenu : FrameLayout {
} }
} }
ll_search.onClick {
runMenuOut {
callBack?.openSearchList()
}
}
//朗读 //朗读
ll_read_aloud.onClick { ll_read_aloud.onClick {
runMenuOut { runMenuOut {
@ -297,7 +300,7 @@ class ReadMenu : FrameLayout {
fun autoPage() fun autoPage()
fun openReplaceRule() fun openReplaceRule()
fun openChapterList() fun openChapterList()
fun openSearchList() fun openSearchActivity(searchWord: String?)
fun showReadStyle() fun showReadStyle()
fun showMoreSetting() fun showMoreSetting()
fun showReadAloudDialog() fun showReadAloudDialog()

@ -11,6 +11,7 @@ import io.legado.app.base.BaseActivity
import io.legado.app.constant.AppConst.timeFormat import io.legado.app.constant.AppConst.timeFormat
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.help.ReadTipConfig import io.legado.app.help.ReadTipConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.widget.BatteryView import io.legado.app.ui.widget.BatteryView
@ -29,6 +30,7 @@ class ContentView(context: Context) : FrameLayout(context) {
private var tvPage: BatteryView? = null private var tvPage: BatteryView? = null
private var tvTotalProgress: BatteryView? = null private var tvTotalProgress: BatteryView? = null
private var tvPageAndTotal: BatteryView? = null private var tvPageAndTotal: BatteryView? = null
private var tvBookName: BatteryView? = null
val headerHeight: Int val headerHeight: Int
get() = if (ReadBookConfig.hideStatusBar) { get() = if (ReadBookConfig.hideStatusBar) {
@ -185,6 +187,19 @@ class ContentView(context: Context) : FrameLayout(context) {
isBattery = false isBattery = false
textSize = 12f textSize = 12f
} }
tvBookName = when (ReadTipConfig.bookName) {
ReadTipConfig.tipHeaderLeft -> bv_header_left
ReadTipConfig.tipHeaderMiddle -> tv_header_middle
ReadTipConfig.tipHeaderRight -> tv_header_right
ReadTipConfig.tipFooterLeft -> bv_footer_left
ReadTipConfig.tipFooterMiddle -> tv_footer_middle
ReadTipConfig.tipFooterRight -> tv_footer_right
else -> null
}
tvBookName?.apply {
isBattery = false
textSize = 12f
}
} }
fun setBg(bg: Drawable?) { fun setBg(bg: Drawable?) {
@ -213,8 +228,8 @@ class ContentView(context: Context) : FrameLayout(context) {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun setProgress(textPage: TextPage) = textPage.apply { fun setProgress(textPage: TextPage) = textPage.apply {
val title = textPage.title tvBookName?.text = ReadBook.book?.name
tvTitle?.text = title tvTitle?.text = textPage.title
tvPage?.text = "${index.plus(1)}/$pageSize" tvPage?.text = "${index.plus(1)}/$pageSize"
tvTotalProgress?.text = readProgress tvTotalProgress?.text = readProgress
tvPageAndTotal?.text = "${index.plus(1)}/$pageSize $readProgress" tvPageAndTotal?.text = "${index.plus(1)}/$pageSize $readProgress"

@ -42,7 +42,13 @@ class SearchAdapter(context: Context, val callBack: CallBack) :
tv_author.text = context.getString(R.string.author_show, searchBook.author) tv_author.text = context.getString(R.string.author_show, searchBook.author)
bv_originCount.setBadgeCount(searchBook.origins.size) bv_originCount.setBadgeCount(searchBook.origins.size)
upLasted(itemView, searchBook.latestChapterTitle) upLasted(itemView, searchBook.latestChapterTitle)
tv_introduce.text = context.getString(R.string.intro_show, searchBook.intro) if (searchBook.intro.isNullOrEmpty()) {
tv_introduce.text =
context.getString(R.string.intro_show_null)
} else {
tv_introduce.text =
context.getString(R.string.intro_show, searchBook.intro)
}
upKind(itemView, searchBook.getKindList()) upKind(itemView, searchBook.getKindList())
iv_cover.load(searchBook.coverUrl, searchBook.name, searchBook.author) iv_cover.load(searchBook.coverUrl, searchBook.name, searchBook.author)
@ -58,8 +64,15 @@ class SearchAdapter(context: Context, val callBack: CallBack) :
context.getString(R.string.author_show, searchBook.author) context.getString(R.string.author_show, searchBook.author)
"origins" -> bv_originCount.setBadgeCount(searchBook.origins.size) "origins" -> bv_originCount.setBadgeCount(searchBook.origins.size)
"last" -> upLasted(itemView, searchBook.latestChapterTitle) "last" -> upLasted(itemView, searchBook.latestChapterTitle)
"intro" -> tv_introduce.text = "intro" -> {
context.getString(R.string.intro_show, searchBook.intro) if (searchBook.intro.isNullOrEmpty()) {
tv_introduce.text =
context.getString(R.string.intro_show_null)
} else {
tv_introduce.text =
context.getString(R.string.intro_show, searchBook.intro)
}
}
"kind" -> upKind(itemView, searchBook.getKindList()) "kind" -> upKind(itemView, searchBook.getKindList())
"cover" -> iv_cover.load( "cover" -> iv_cover.load(
searchBook.coverUrl, searchBook.coverUrl,

@ -1,93 +1,264 @@
package io.legado.app.ui.book.searchContent package io.legado.app.ui.book.searchContent
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isGone import com.hankcs.hanlp.HanLP
import androidx.fragment.app.Fragment import io.legado.app.App
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.lib.theme.primaryTextColor import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.ui.widget.recycler.UpLinearLayoutManager
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.ColorUtils
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.gone import io.legado.app.utils.observeEvent
import io.legado.app.utils.visible import kotlinx.android.synthetic.main.activity_search_list.*
import kotlinx.android.synthetic.main.activity_chapter_list.* import kotlinx.android.synthetic.main.view_search.*
import kotlinx.android.synthetic.main.view_tab_layout.* import kotlinx.coroutines.*
import org.jetbrains.anko.sdk27.listeners.onClick
class SearchListActivity : VMBaseActivity<SearchListViewModel>(R.layout.activity_search_list) { class SearchListActivity : VMBaseActivity<SearchListViewModel>(R.layout.activity_search_list),
// todo: 完善搜索界面UI SearchListAdapter.Callback {
override val viewModel: SearchListViewModel override val viewModel: SearchListViewModel
get() = getViewModel(SearchListViewModel::class.java) get() = getViewModel(SearchListViewModel::class.java)
private var searchView: SearchView? = null lateinit var adapter: SearchListAdapter
private lateinit var mLayoutManager: UpLinearLayoutManager
private var searchResultCounts = 0
private var durChapterIndex = 0
private var searchResultList: MutableList<SearchResult> = mutableListOf()
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
tab_layout.isTabIndicatorFullWidth = false val bbg = bottomBackground
tab_layout.setSelectedTabIndicatorColor(accentColor) val btc = getPrimaryTextColor(ColorUtils.isColorLight(bbg))
ll_search_base_info.setBackgroundColor(bbg)
tv_current_search_info.setTextColor(btc)
iv_search_content_top.setColorFilter(btc)
iv_search_content_bottom.setColorFilter(btc)
initSearchView()
initRecyclerView()
initView()
intent.getStringExtra("bookUrl")?.let { intent.getStringExtra("bookUrl")?.let {
viewModel.initBook(it) { viewModel.initBook(it) {
view_pager.adapter = TabFragmentPageAdapter(supportFragmentManager) initBook()
tab_layout.setupWithViewPager(view_pager)
} }
} }
} }
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { private fun initSearchView() {
menuInflater.inflate(R.menu.search_view, menu) ATH.setTint(search_view, primaryTextColor)
val search = menu.findItem(R.id.menu_search) search_view.onActionViewExpanded()
searchView = search.actionView as SearchView search_view.isSubmitButtonEnabled = true
ATH.setTint(searchView!!, primaryTextColor) search_view.queryHint = getString(R.string.search)
searchView?.maxWidth = resources.displayMetrics.widthPixels search_view.clearFocus()
searchView?.onActionViewCollapsed() search_view.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
searchView?.setOnCloseListener {
tab_layout.visible()
//to do clean
false
}
searchView?.setOnSearchClickListener { tab_layout.gone() }
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
if (viewModel.lastQuery != query){ if (viewModel.lastQuery != query) {
viewModel.startContentSearch(query) startContentSearch(query)
} }
return false return false
} }
override fun onQueryTextChange(newText: String): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
return false return false
} }
}) })
return super.onCompatCreateOptionsMenu(menu)
} }
private inner class TabFragmentPageAdapter(fm: FragmentManager) : private fun initRecyclerView() {
FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { adapter = SearchListAdapter(this, this)
override fun getItem(position: Int): Fragment { mLayoutManager = UpLinearLayoutManager(this)
return SearchListFragment() recycler_view.layoutManager = mLayoutManager
recycler_view.addItemDecoration(VerticalDivider(this))
recycler_view.adapter = adapter
}
private fun initView() {
iv_search_content_top.onClick { mLayoutManager.scrollToPositionWithOffset(0, 0) }
iv_search_content_bottom.onClick {
if (adapter.itemCount > 0) {
mLayoutManager.scrollToPositionWithOffset(adapter.itemCount - 1, 0)
}
}
}
@SuppressLint("SetTextI18n")
private fun initBook() {
launch {
tv_current_search_info.text = "搜索结果:$searchResultCounts"
viewModel.book?.let {
initCacheFileNames(it)
durChapterIndex = it.durChapterIndex
intent.getStringExtra("searchWord")?.let { searchWord ->
search_view.setQuery(searchWord, true)
}
}
}
}
private fun initCacheFileNames(book: Book) {
launch(Dispatchers.IO) {
adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book))
withContext(Dispatchers.Main) {
adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true)
}
}
}
override fun observeLiveBus() {
observeEvent<BookChapter>(EventBus.SAVE_CONTENT) { chapter ->
viewModel.book?.bookUrl?.let { bookUrl ->
if (chapter.bookUrl == bookUrl) {
adapter.cacheFileNames.add(BookHelp.formatChapterName(chapter))
adapter.notifyItemChanged(chapter.index, true)
}
}
} }
}
override fun getCount(): Int { @SuppressLint("SetTextI18n")
return 1 fun startContentSearch(newText: String) {
// 按章节搜索内容
if (!newText.isBlank()) {
adapter.clearItems()
searchResultList.clear()
searchResultCounts = 0
viewModel.lastQuery = newText
var searchResults = listOf<SearchResult>()
launch(Dispatchers.Main) {
App.db.bookChapterDao().getChapterList(viewModel.bookUrl).map { chapter ->
val job = async(Dispatchers.IO) {
if (isLocalBook || adapter.cacheFileNames.contains(
BookHelp.formatChapterName(
chapter
)
)
) {
searchResults = searchChapter(newText, chapter)
}
}
job.await()
if (searchResults.isNotEmpty()) {
searchResultList.addAll(searchResults)
tv_current_search_info.text = "搜索结果:$searchResultCounts"
adapter.addItems(searchResults)
searchResults = listOf()
}
}
}
} }
}
override fun getPageTitle(position: Int): CharSequence? { private suspend fun searchChapter(query: String, chapter: BookChapter?): List<SearchResult> {
return "Search" val searchResults: MutableList<SearchResult> = mutableListOf()
var positions: List<Int>
var replaceContents: List<String>? = null
var totalContents: String
if (chapter != null) {
viewModel.book?.let { bookSource ->
val bookContent = BookHelp.getContent(bookSource, chapter)
if (bookContent != null) {
//搜索替换后的正文
val job = async(Dispatchers.IO) {
chapter.title = when (AppConfig.chineseConverterType) {
1 -> HanLP.convertToSimplifiedChinese(chapter.title)
2 -> HanLP.convertToTraditionalChinese(chapter.title)
else -> chapter.title
}
replaceContents = BookHelp.disposeContent(
chapter.title,
bookSource.name,
bookSource.bookUrl,
bookContent,
bookSource.useReplaceRule
)
}
job.await()
while (replaceContents == null) {
delay(100L)
}
totalContents = replaceContents!!.joinToString("")
positions = searchPosition(totalContents, query)
var count = 1
positions.map {
val construct = constructText(totalContents, it, query)
val result = SearchResult(
index = searchResultCounts,
indexWithinChapter = count,
text = construct[1] as String,
chapterTitle = chapter.title,
query = query,
chapterIndex = chapter.index,
newPosition = construct[0] as Int,
contentPosition = it
)
count += 1
searchResultCounts += 1
searchResults.add(result)
}
}
}
} }
return searchResults
}
private fun searchPosition(content: String, pattern: String): List<Int> {
val position: MutableList<Int> = mutableListOf()
var index = content.indexOf(pattern)
while (index >= 0) {
position.add(index)
index = content.indexOf(pattern, index + 1)
}
return position
} }
override fun onBackPressed() { private fun constructText(content: String, position: Int, query: String): Array<Any> {
if (tab_layout.isGone) { // 构建关键词周边文字,在搜索结果里显示
searchView?.onActionViewCollapsed() // todo: 判断段落,只在关键词所在段落内分割
tab_layout.visible() // todo: 利用标点符号分割完整的句
} else { // todo: length和设置结合,自由调整周边文字长度
super.onBackPressed() val length = 20
var po1 = position - length
var po2 = position + query.length + length
if (po1 < 0) {
po1 = 0
}
if (po2 > content.length) {
po2 = content.length
} }
val newPosition = position - po1
val newText = content.substring(po1, po2)
return arrayOf(newPosition, newText)
} }
val isLocalBook: Boolean
get() = viewModel.book?.isLocalBook() == true
override fun openSearchResult(searchResult: SearchResult) {
val searchData = Intent()
searchData.putExtra("index", searchResult.chapterIndex)
searchData.putExtra("contentPosition", searchResult.contentPosition)
searchData.putExtra("query", searchResult.query)
searchData.putExtra("indexWithinChapter", searchResult.indexWithinChapter)
setResult(RESULT_OK, searchData)
finish()
}
override fun durChapterIndex(): Int {
return durChapterIndex
}
} }

@ -1,21 +1,12 @@
package io.legado.app.ui.book.searchContent package io.legado.app.ui.book.searchContent
import android.content.Context import android.content.Context
import android.os.Build
import android.text.Html
import android.util.Log
import android.view.View
import androidx.annotation.RequiresApi
import androidx.core.text.HtmlCompat
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.getCompatColor import io.legado.app.utils.getCompatColor
import io.legado.app.utils.visible import io.legado.app.utils.hexString
import kotlinx.android.synthetic.main.item_bookmark.view.*
import kotlinx.android.synthetic.main.item_search_list.view.* import kotlinx.android.synthetic.main.item_search_list.view.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
@ -23,15 +14,15 @@ class SearchListAdapter(context: Context, val callback: Callback) :
SimpleRecyclerAdapter<SearchResult>(context, R.layout.item_search_list) { SimpleRecyclerAdapter<SearchResult>(context, R.layout.item_search_list) {
val cacheFileNames = hashSetOf<String>() val cacheFileNames = hashSetOf<String>()
val textColor = context.getCompatColor(R.color.primaryText).hexString.substring(2)
val accentColor = context.accentColor.hexString.substring(2)
override fun convert(holder: ItemViewHolder, item: SearchResult, payloads: MutableList<Any>) { override fun convert(holder: ItemViewHolder, item: SearchResult, payloads: MutableList<Any>) {
with(holder.itemView) { with(holder.itemView) {
val isDur = callback.durChapterIndex() == item.chapterIndex val isDur = callback.durChapterIndex() == item.chapterIndex
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
tv_search_result.text = item.parseText(item.presentText) tv_search_result.text = item.getHtmlCompat(textColor, accentColor)
if (isDur){ tv_search_result.paint.isFakeBoldText = isDur
tv_search_result.paint.isFakeBoldText = true
}
} }
} }
} }

@ -1,239 +0,0 @@
package io.legado.app.ui.book.searchContent
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.lifecycle.LiveData
import com.hankcs.hanlp.HanLP
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseFragment
import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.widget.recycler.UpLinearLayoutManager
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.ColorUtils
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.observeEvent
import kotlinx.android.synthetic.main.fragment_search_list.*
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import org.jetbrains.anko.sdk27.listeners.onClick
import java.util.regex.Pattern
class SearchListFragment : VMBaseFragment<SearchListViewModel>(R.layout.fragment_search_list),
SearchListAdapter.Callback,
SearchListViewModel.SearchListCallBack{
override val viewModel: SearchListViewModel
get() = getViewModelOfActivity(SearchListViewModel::class.java)
lateinit var adapter: SearchListAdapter
private lateinit var mLayoutManager: UpLinearLayoutManager
private var searchResultCounts = 0
private var durChapterIndex = 0
private var searchResultList: MutableList<SearchResult> = mutableListOf()
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
viewModel.searchCallBack = this
val bbg = bottomBackground
val btc = requireContext().getPrimaryTextColor(ColorUtils.isColorLight(bbg))
ll_search_base_info.setBackgroundColor(bbg)
tv_current_search_info.setTextColor(btc)
iv_search_content_top.setColorFilter(btc)
iv_search_content_bottom.setColorFilter(btc)
initRecyclerView()
initView()
initBook()
}
private fun initRecyclerView() {
adapter = SearchListAdapter(requireContext(), this)
mLayoutManager = UpLinearLayoutManager(requireContext())
recycler_view.layoutManager = mLayoutManager
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
}
private fun initView() {
iv_search_content_top.onClick { mLayoutManager.scrollToPositionWithOffset(0, 0) }
iv_search_content_bottom.onClick {
if (adapter.itemCount > 0) {
mLayoutManager.scrollToPositionWithOffset(adapter.itemCount - 1, 0)
}
}
}
@SuppressLint("SetTextI18n")
private fun initBook() {
launch {
tv_current_search_info.text = "搜索结果:$searchResultCounts"
viewModel.book?.let {
initCacheFileNames(it)
durChapterIndex = it.durChapterIndex
}
}
}
private fun initCacheFileNames(book: Book) {
launch(IO) {
adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book))
withContext(Main) {
adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true)
}
}
}
override fun observeLiveBus() {
observeEvent<BookChapter>(EventBus.SAVE_CONTENT) { chapter ->
viewModel.book?.bookUrl?.let { bookUrl ->
if (chapter.bookUrl == bookUrl) {
adapter.cacheFileNames.add(BookHelp.formatChapterName(chapter))
adapter.notifyItemChanged(chapter.index, true)
}
}
}
}
@SuppressLint("SetTextI18n")
override fun startContentSearch(newText: String) {
// 按章节搜索内容
if (!newText.isBlank()) {
adapter.clearItems()
searchResultList.clear()
searchResultCounts = 0
viewModel.lastQuery = newText
var searchResults = listOf<SearchResult>()
launch(Main){
App.db.bookChapterDao().getChapterList(viewModel.bookUrl).map{ chapter ->
val job = async(IO){
if (isLocalBook || adapter.cacheFileNames.contains(BookHelp.formatChapterName(chapter))) {
searchResults = searchChapter(newText, chapter)
}
}
job.await()
if (searchResults.isNotEmpty()){
searchResultList.addAll(searchResults)
tv_current_search_info.text = "搜索结果:$searchResultCounts"
adapter.addItems(searchResults)
searchResults = listOf<SearchResult>()
}
}
}
}
}
private suspend fun searchChapter(query: String, chapter: BookChapter?): List<SearchResult> {
val searchResults: MutableList<SearchResult> = mutableListOf()
var positions : List<Int> = listOf()
var replaceContents: List<String>? = null
var totalContents = ""
if (chapter != null){
viewModel.book?.let { bookSource ->
val bookContent = BookHelp.getContent(bookSource, chapter)
if (bookContent != null){
//搜索替换后的正文
val job = async(IO) {
chapter.title = when (AppConfig.chineseConverterType) {
1 -> HanLP.convertToSimplifiedChinese(chapter.title)
2 -> HanLP.convertToTraditionalChinese(chapter.title)
else -> chapter.title
}
replaceContents = BookHelp.disposeContent(
chapter.title,
bookSource.name,
bookSource.bookUrl,
bookContent,
bookSource.useReplaceRule
)
}
job.await()
while (replaceContents == null){
delay(100L)
}
totalContents = replaceContents!!.joinToString("")
positions = searchPosition(totalContents, query)
var count = 1
positions.map{
val construct = constructText(totalContents, it, query)
val result = SearchResult(index = searchResultCounts,
indexWithinChapter = count,
text = construct[1] as String,
chapterTitle = chapter.title,
query = query,
chapterIndex = chapter.index,
newPosition = construct[0] as Int,
contentPosition = it
)
count += 1
searchResultCounts += 1
searchResults.add(result)
}
}
}
}
return searchResults
}
private fun searchPosition(content: String, pattern: String): List<Int> {
val position : MutableList<Int> = mutableListOf()
var index = content.indexOf(pattern)
while(index >= 0){
position.add(index)
index = content.indexOf(pattern, index + 1);
}
return position
}
private fun constructText(content: String, position: Int, query: String): Array<Any>{
// 构建关键词周边文字,在搜索结果里显示
// todo: 判断段落,只在关键词所在段落内分割
// todo: 利用标点符号分割完整的句
// todo: length和设置结合,自由调整周边文字长度
val length = 20
var po1 = position - length
var po2 = position + query.length + length
if (po1 <0) {
po1 = 0
}
if (po2 > content.length){
po2 = content.length
}
val newPosition = position - po1
val newText = content.substring(po1, po2)
return arrayOf(newPosition, newText)
}
val isLocalBook: Boolean
get() = viewModel.book?.isLocalBook() == true
override fun openSearchResult(searchResult: SearchResult) {
val searchData = Intent()
searchData.putExtra("index", searchResult.chapterIndex)
searchData.putExtra("contentPosition", searchResult.contentPosition)
searchData.putExtra("query", searchResult.query)
searchData.putExtra("indexWithinChapter", searchResult.indexWithinChapter)
activity?.setResult(RESULT_OK, searchData)
activity?.finish()
}
override fun durChapterIndex(): Int {
return durChapterIndex
}
}

@ -9,7 +9,6 @@ import io.legado.app.data.entities.Book
class SearchListViewModel(application: Application) : BaseViewModel(application) { class SearchListViewModel(application: Application) : BaseViewModel(application) {
var bookUrl: String = "" var bookUrl: String = ""
var book: Book? = null var book: Book? = null
var searchCallBack: SearchListCallBack? = null
var lastQuery: String = "" var lastQuery: String = ""
fun initBook(bookUrl: String, success: () -> Unit) { fun initBook(bookUrl: String, success: () -> Unit) {
@ -21,13 +20,4 @@ class SearchListViewModel(application: Application) : BaseViewModel(application)
} }
} }
fun startContentSearch(newText: String) {
searchCallBack?.startContentSearch(newText)
}
interface SearchListCallBack {
fun startContentSearch(newText: String)
}
} }

@ -2,10 +2,6 @@ package io.legado.app.ui.book.searchContent
import android.text.Spanned import android.text.Spanned
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import io.legado.app.App
import io.legado.app.R
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.hexString
data class SearchResult( data class SearchResult(
var index: Int = 0, var index: Int = 0,
@ -19,25 +15,25 @@ data class SearchResult(
var newPosition: Int = 0, var newPosition: Int = 0,
var contentPosition: Int =0 var contentPosition: Int =0
) { ) {
val presentText: String
get(){
return colorPresentText(newPosition, query, text) +
"<font color=#0000ff>($chapterTitle)</font>"
}
fun colorPresentText(position: Int, center: String, targetText: String): String { fun getHtmlCompat(textColor: String, accentColor: String): Spanned {
val html = colorPresentText(newPosition, query, text, textColor, accentColor) +
"<font color=#${accentColor}>($chapterTitle)</font>"
return HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
private fun colorPresentText(
position: Int,
center: String,
targetText: String,
textColor: String,
accentColor: String
): String {
val sub1 = text.substring(0, position) val sub1 = text.substring(0, position)
val sub2 = text.substring(position + center.length, targetText.length) val sub2 = text.substring(position + center.length, targetText.length)
val textColor = App.INSTANCE.getCompatColor(R.color.primaryText).hexString
return "<font color=#${textColor}>$sub1</font>" + return "<font color=#${textColor}>$sub1</font>" +
"<font color=#ff0000>$center</font>" + "<font color=#${accentColor}>$center</font>" +
"<font color=#${textColor}>$sub2</font>" "<font color=#${textColor}>$sub2</font>"
} }
fun parseText(targetText: String): Spanned {
return HtmlCompat.fromHtml(targetText, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
} }

@ -5,9 +5,8 @@ import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.util.DisplayMetrics
import android.view.MenuItem import android.view.*
import android.view.SubMenu
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
@ -19,8 +18,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.BuildConfig import io.legado.app.BuildConfig
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.EventBus
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentDataHelp
import io.legado.app.lib.dialogs.* import io.legado.app.lib.dialogs.*
@ -39,8 +40,12 @@ import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_book_source.* import kotlinx.android.synthetic.main.activity_book_source.*
import kotlinx.android.synthetic.main.activity_book_source.recycler_view
import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.dialog_progressbar_view.*
import kotlinx.android.synthetic.main.dialog_progressbar_view.tv_footer_left
import kotlinx.android.synthetic.main.view_search.* import kotlinx.android.synthetic.main.view_search.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
@ -384,6 +389,24 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
}.show().applyTint() }.show().applyTint()
} }
override fun observeLiveBus() {
observeEvent<Int>(EventBus.CHECK_INIT) { max->
val bundle = Bundle()
bundle.putInt("maxProgress", max)
CheckSourceDialog().apply {
arguments = bundle
}.show(supportFragmentManager, "CheckDialog")
}
observeEvent<Int>(EventBus.CHECK_DONE) {
groups.map { group->
if (group.contains("失效")) {
search_view.setQuery("失效", true)
toast("发现有失效书源,已为您自动筛选!")
}
}
}
}
override fun upCountView() { override fun upCountView() {
select_action_bar.upCountView(adapter.getSelection().size, adapter.getActualItemCount()) select_action_bar.upCountView(adapter.getSelection().size, adapter.getActualItemCount())
} }
@ -478,4 +501,47 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
search_view.setQuery("", true) search_view.setQuery("", true)
} }
} }
class CheckSourceDialog : BaseDialogFragment() {
override fun onStart() {
super.onStart()
val dm = DisplayMetrics()
activity?.windowManager?.defaultDisplay?.getMetrics(dm)
dialog?.window?.setLayout(
(dm.widthPixels * 0.9).toInt(),
(dm.heightPixels * 0.14).toInt()
)
dialog?.setCancelable(false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_progressbar_view, container)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
arguments?.let { bundle ->
val maxProgress = bundle.getInt("maxProgress")
ck_progress_text.text = getString(R.string.progress_show, "", 0, maxProgress)
ck_progress.max = maxProgress
observeEvent<Int>(EventBus.CHECK_UP_PROGRESS) { progress->
ck_progress.progress = progress
}
observeEvent<String>(EventBus.CHECK_UP_PROGRESS_STRING) {
ck_progress_text.text = it
}
observeEvent<Int>(EventBus.CHECK_DONE) {
dismiss()
}
tv_footer_left.onClick {
CheckSource.stop(requireContext()).apply {
dismiss()
}
}
}
}
}
} }

@ -11,10 +11,7 @@ import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.ACache import io.legado.app.utils.*
import io.legado.app.utils.dp
import io.legado.app.utils.gone
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_fillet_text.view.* import kotlinx.android.synthetic.main.item_fillet_text.view.*
import kotlinx.android.synthetic.main.item_find_book.view.* import kotlinx.android.synthetic.main.item_find_book.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -55,12 +52,12 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca
.inflate(R.layout.item_fillet_text, gl_child, false) .inflate(R.layout.item_fillet_text, gl_child, false)
gl_child.addView(tv) gl_child.addView(tv)
tv.text_view.text = kind.title tv.text_view.text = kind.title
tv.text_view.onClick { if (!kind.url.isNullOrEmpty()) {
kind.url?.let { kindUrl -> tv.text_view.onClick {
callBack.openExplore( callBack.openExplore(
item.bookSourceUrl, item.bookSourceUrl,
kind.title, kind.title,
kindUrl kind.url.toString()
) )
} }
} }

@ -123,6 +123,26 @@ class FontSelectDialog : BaseDialogFragment(),
} }
} }
private fun getLocalFonts(): ArrayList<DocItem> {
val fontItems = arrayListOf<DocItem>()
val fontDir =
FileUtils.createFolderIfNotExist(requireContext().externalFilesDir, "font")
fontDir.listFiles { pathName ->
pathName.name.toLowerCase(Locale.getDefault()).matches(fontRegex)
}?.forEach {
fontItems.add(
DocItem(
it.name,
it.extension,
it.length(),
Date(it.lastModified()),
Uri.parse(it.absolutePath)
)
)
}
return fontItems
}
private fun loadFontFiles(doc: DocumentFile) { private fun loadFontFiles(doc: DocumentFile) {
execute { execute {
val fontItems = arrayListOf<DocItem>() val fontItems = arrayListOf<DocItem>()
@ -132,6 +152,7 @@ class FontSelectDialog : BaseDialogFragment(),
fontItems.add(item) fontItems.add(item)
} }
} }
fontItems.addAll(getLocalFonts())
fontItems.sortedBy { it.name } fontItems.sortedBy { it.name }
}.onSuccess { }.onSuccess {
adapter?.setItems(it) adapter?.setItems(it)
@ -167,6 +188,7 @@ class FontSelectDialog : BaseDialogFragment(),
) )
) )
} }
fontItems.addAll(getLocalFonts())
fontItems.sortedBy { it.name } fontItems.sortedBy { it.name }
}.onSuccess { }.onSuccess {
adapter?.setItems(it) adapter?.setItems(it)

@ -1,19 +1,74 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<io.legado.app.ui.widget.TitleBar <io.legado.app.ui.widget.TitleBar
android:id="@+id/title_bar" android:id="@+id/title_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:contentLayout="@layout/view_tab_layout"/> app:contentLayout="@layout/view_search"
app:contentInsetRight="24dp"
app:layout_constraintTop_toTopOf="parent" />
<androidx.viewpager.widget.ViewPager <io.legado.app.ui.widget.recycler.scroller.FastScrollRecyclerView
android:id="@+id/view_pager" android:id="@+id/recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="0dp"
android:overScrollMode="never"
app:layout_constraintBottom_toTopOf="@+id/ll_search_base_info"
app:layout_constraintTop_toBottomOf="@id/title_bar" />
</LinearLayout> <LinearLayout
android:id="@+id/ll_search_base_info"
android:layout_width="match_parent"
android:layout_height="36dp"
android:background="@color/background"
android:elevation="5dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="10dp"
android:paddingRight="10dp"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/tv_current_search_info"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:ellipsize="middle"
android:gravity="center_vertical"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:singleLine="true"
android:textColor="@color/primaryText"
android:textSize="12sp" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_search_content_top"
android:layout_width="36dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/go_to_top"
android:src="@drawable/ic_arrow_drop_up"
android:tooltipText="@string/go_to_top"
app:tint="@color/primaryText"
tools:ignore="UnusedAttribute" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_search_content_bottom"
android:layout_width="36dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/go_to_bottom"
android:src="@drawable/ic_arrow_drop_down"
android:tooltipText="@string/go_to_bottom"
app:tint="@color/primaryText"
tools:ignore="UnusedAttribute" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<io.legado.app.lib.theme.view.ATEProgressBar
android:id="@+id/ck_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp"
android:layout_marginRight="60dp"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/ck_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="20dp"
app:layout_constraintBottom_toBottomOf="@+id/ck_progress"
app:layout_constraintEnd_toEndOf="@+id/ck_progress" />
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/tv_footer_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/ck_progress"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlHardcoded" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants">
<io.legado.app.ui.widget.recycler.scroller.FastScrollRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:overScrollMode="never"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/ll_search_base_info" />
<LinearLayout
android:id="@+id/ll_search_base_info"
android:layout_width="match_parent"
android:layout_height="36dp"
android:background="@color/background"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:elevation="5dp"
android:gravity="center_vertical"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/tv_current_search_info"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:ellipsize="middle"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:singleLine="true"
android:gravity="center_vertical"
android:textColor="@color/primaryText"
android:textSize="12sp" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_search_content_top"
android:layout_width="36dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/go_to_top"
android:src="@drawable/ic_arrow_drop_up"
android:tooltipText="@string/go_to_top"
app:tint="@color/primaryText"
tools:ignore="UnusedAttribute" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_search_content_bottom"
android:layout_width="36dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/go_to_bottom"
android:src="@drawable/ic_arrow_drop_down"
android:tooltipText="@string/go_to_bottom"
app:tint="@color/primaryText"
tools:ignore="UnusedAttribute" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -92,6 +92,26 @@
android:paddingStart="32dp" android:paddingStart="32dp"
android:paddingEnd="32dp"> android:paddingEnd="32dp">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/search_content"
android:src="@drawable/ic_search"
android:tint="@color/primaryText"
android:tooltipText="@string/search_content"
app:backgroundTint="@color/background_menu"
app:elevation="2dp"
app:fabSize="mini"
app:pressedTranslationZ="2dp"
tools:ignore="UnusedAttribute" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAutoPage" android:id="@+id/fabAutoPage"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -265,44 +285,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="2" /> android:layout_weight="2" />
<!--搜索按钮-->
<LinearLayout
android:id="@+id/ll_search"
android:layout_width="60dp"
android:layout_height="50dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/search_content"
android:focusable="true"
android:orientation="vertical"
android:paddingBottom="7dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_search"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:contentDescription="@string/search_content"
android:src="@drawable/ic_search"
app:tint="@color/primaryText"
tools:ignore="NestedWeights" />
<TextView
android:id="@+id/tv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="3dp"
android:maxLines="1"
android:text="@string/search_content"
android:textColor="@color/primaryText"
android:textSize="12sp" />
</LinearLayout>
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2" />
<!--调节按钮--> <!--调节按钮-->
<LinearLayout <LinearLayout
android:id="@+id/ll_read_aloud" android:id="@+id/ll_read_aloud"

@ -24,4 +24,10 @@
android:checkable="true" android:checkable="true"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_load_info"
android:title="@string/load_info"
android:checkable="true"
app:showAsAction="never" />
</menu> </menu>

@ -13,6 +13,10 @@
android:id="@+id/menu_aloud" android:id="@+id/menu_aloud"
android:title="@string/read_aloud" /> android:title="@string/read_aloud" />
<item
android:id="@+id/menu_search_content"
android:title="@string/search_content" />
<item <item
android:id="@+id/menu_browser" android:id="@+id/menu_browser"
android:title="@string/browser" /> android:title="@string/browser" />

@ -3,6 +3,12 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_new_group"
android:title="@string/diy_source_group"
app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
<item <item
android:id="@+id/menu_select_all" android:id="@+id/menu_select_all"
android:title="@string/select_all" android:title="@string/select_all"

@ -35,6 +35,17 @@
<item>系統等寬字體</item> <item>系統等寬字體</item>
</string-array> </string-array>
<string-array name="read_tip">
<item></item>
<item>标题</item>
<item>时间</item>
<item>电量</item>
<item>页数</item>
<item>进度</item>
<item>页数及进度</item>
<item>書名</item>
</string-array>
<string-array name="text_font_weight"> <string-array name="text_font_weight">
<item>正常</item> <item>正常</item>
<item>粗體</item> <item>粗體</item>

@ -10,6 +10,8 @@
<string name="menu_import_old">導入閲讀數據</string> <string name="menu_import_old">導入閲讀數據</string>
<string name="mkdirs">創建子文件夾</string> <string name="mkdirs">創建子文件夾</string>
<string name="mkdirs_description">創建 legado 文件夾作爲備份路徑</string> <string name="mkdirs_description">創建 legado 文件夾作爲備份路徑</string>
<string name="webdav_cache_backup">離線緩存書籍備份</string>
<string name="webdav_cache_backup_s">導出本地同時備份到legado文件夾下exports目錄</string>
<string name="backup_path">備份路徑</string> <string name="backup_path">備份路徑</string>
<string name="menu_import_old_version">導入舊版數據</string> <string name="menu_import_old_version">導入舊版數據</string>
<string name="menu_import_github">導入 Github 數據</string> <string name="menu_import_github">導入 Github 數據</string>
@ -94,6 +96,8 @@
閲讀3.0下載地址:\nhttps://play.google.com/store/apps/details?id=io.legado.play.release 閲讀3.0下載地址:\nhttps://play.google.com/store/apps/details?id=io.legado.play.release
</string> </string>
<string name="version_name">Version %s</string> <string name="version_name">Version %s</string>
<string name="pt_background_verification">後臺校驗書源</string>
<string name="ps_background_verification">打開后可以在校驗書源時自由操作</string>
<string name="pt_auto_refresh">自動刷新</string> <string name="pt_auto_refresh">自動刷新</string>
<string name="ps_auto_refresh">打開程式時自動更新書輯</string> <string name="ps_auto_refresh">打開程式時自動更新書輯</string>
<string name="pt_auto_download">自動下載最新章節</string> <string name="pt_auto_download">自動下載最新章節</string>
@ -223,6 +227,7 @@
<string name="load_error_retry">載入失敗,點擊重試</string> <string name="load_error_retry">載入失敗,點擊重試</string>
<string name="book_intro">內容簡介</string> <string name="book_intro">內容簡介</string>
<string name="intro_show">簡介: %s</string> <string name="intro_show">簡介: %s</string>
<string name="intro_show_null">簡介: 暫無簡介</string>
<string name="open_from_other">打開外部書籍</string> <string name="open_from_other">打開外部書籍</string>
<string name="origin_show">來源: %s</string> <string name="origin_show">來源: %s</string>
<string name="import_replace_rule">本地導入</string> <string name="import_replace_rule">本地導入</string>
@ -288,7 +293,7 @@
<string name="padding_right">右邊距</string> <string name="padding_right">右邊距</string>
<string name="check_book_source">校驗書源</string> <string name="check_book_source">校驗書源</string>
<string name="check_select_source">校驗所選</string> <string name="check_select_source">校驗所選</string>
<string name="progress_show">進度 %1$d/%2$d</string> <string name="progress_show">%1$s 進度 %2$d/%3$d</string>
<string name="tts_fix">請安裝並選擇中文 TTS!</string> <string name="tts_fix">請安裝並選擇中文 TTS!</string>
<string name="tts_init_failed">TTS 初始化失敗!</string> <string name="tts_init_failed">TTS 初始化失敗!</string>
<string name="jf_convert">簡繁轉換</string> <string name="jf_convert">簡繁轉換</string>
@ -563,6 +568,7 @@
<string name="export_selection">導出所選</string> <string name="export_selection">導出所選</string>
<string name="export">導出</string> <string name="export">導出</string>
<string name="load_toc">加載目錄</string> <string name="load_toc">加載目錄</string>
<string name="load_info">加載詳情頁</string>
<string name="tts">TTS</string> <string name="tts">TTS</string>
<string name="web_dav_pw">WebDav 密碼</string> <string name="web_dav_pw">WebDav 密碼</string>
<string name="web_dav_pw_s">輸入你的 WebDav 授權密碼</string> <string name="web_dav_pw_s">輸入你的 WebDav 授權密碼</string>
@ -761,6 +767,6 @@
<string name="select_theme">切換默認主題</string> <string name="select_theme">切換默認主題</string>
<string name="share_selected_source">分享選中書源</string> <string name="share_selected_source">分享選中書源</string>
<string name="sort_by_lastUppdateTime">時間排序</string> <string name="sort_by_lastUppdateTime">時間排序</string>
<string name="search_content">搜索</string> <string name="search_content">全文搜索</string>
</resources> </resources>

@ -89,6 +89,7 @@
<item>頁數</item> <item>頁數</item>
<item>進度</item> <item>進度</item>
<item>頁數及進度</item> <item>頁數及進度</item>
<item>書名</item>
</string-array> </string-array>
<string-array name="text_font_weight"> <string-array name="text_font_weight">

@ -10,6 +10,8 @@
<string name="menu_import_old">匯入閱讀資料</string> <string name="menu_import_old">匯入閱讀資料</string>
<string name="mkdirs">建立子資料夾</string> <string name="mkdirs">建立子資料夾</string>
<string name="mkdirs_description">建立legado資料夾作為備份資料夾</string> <string name="mkdirs_description">建立legado資料夾作為備份資料夾</string>
<string name="webdav_cache_backup">離線緩存書籍備份</string>
<string name="webdav_cache_backup_s">導出本地同時備份到legado文件夾下exports目錄</string>
<string name="backup_path">備份路徑</string> <string name="backup_path">備份路徑</string>
<string name="menu_import_old_version">匯入舊版資料</string> <string name="menu_import_old_version">匯入舊版資料</string>
<string name="menu_import_github">匯入Github資料</string> <string name="menu_import_github">匯入Github資料</string>
@ -94,6 +96,8 @@
閱讀3.0下載網址:\nhttps://play.google.com/store/apps/details?id=io.legado.play.release 閱讀3.0下載網址:\nhttps://play.google.com/store/apps/details?id=io.legado.play.release
</string> </string>
<string name="version_name">Version %s</string> <string name="version_name">Version %s</string>
<string name="pt_background_verification">後臺校驗書源</string>
<string name="ps_background_verification">打開后可以在校驗書源時自由操作</string>
<string name="pt_auto_refresh">自動重新整理</string> <string name="pt_auto_refresh">自動重新整理</string>
<string name="ps_auto_refresh">打開軟體時自動更新書籍</string> <string name="ps_auto_refresh">打開軟體時自動更新書籍</string>
<string name="pt_auto_download">自動下載最新章節</string> <string name="pt_auto_download">自動下載最新章節</string>
@ -223,6 +227,7 @@
<string name="load_error_retry">載入失敗,點擊重試</string> <string name="load_error_retry">載入失敗,點擊重試</string>
<string name="book_intro">內容簡介</string> <string name="book_intro">內容簡介</string>
<string name="intro_show">簡介:%s</string> <string name="intro_show">簡介:%s</string>
<string name="intro_show_null">簡介: 暫無簡介</string>
<string name="open_from_other">打開外部書籍</string> <string name="open_from_other">打開外部書籍</string>
<string name="origin_show">來源: %s</string> <string name="origin_show">來源: %s</string>
<string name="import_replace_rule">本機匯入</string> <string name="import_replace_rule">本機匯入</string>
@ -288,7 +293,7 @@
<string name="padding_right">右邊距</string> <string name="padding_right">右邊距</string>
<string name="check_book_source">校驗書源</string> <string name="check_book_source">校驗書源</string>
<string name="check_select_source">校驗所選</string> <string name="check_select_source">校驗所選</string>
<string name="progress_show">進度 %1$d/%2$d</string> <string name="progress_show">%1$s 進度 %2$d/%3$d</string>
<string name="tts_fix">請安裝並選擇中文TTS!</string> <string name="tts_fix">請安裝並選擇中文TTS!</string>
<string name="tts_init_failed">TTS初始化失敗!</string> <string name="tts_init_failed">TTS初始化失敗!</string>
<string name="jf_convert">簡繁轉換</string> <string name="jf_convert">簡繁轉換</string>
@ -564,6 +569,7 @@
<string name="export_selection">匯出所選</string> <string name="export_selection">匯出所選</string>
<string name="export">匯出</string> <string name="export">匯出</string>
<string name="load_toc">載入目錄</string> <string name="load_toc">載入目錄</string>
<string name="load_info">載入詳情頁</string>
<string name="tts">TTS</string> <string name="tts">TTS</string>
<string name="web_dav_pw">WebDav 密碼</string> <string name="web_dav_pw">WebDav 密碼</string>
<string name="web_dav_pw_s">輸入你的WebDav授權密碼</string> <string name="web_dav_pw_s">輸入你的WebDav授權密碼</string>
@ -761,7 +767,6 @@
<string name="select_theme">切換默認主題</string> <string name="select_theme">切換默認主題</string>
<string name="share_selected_source">分享選中書源</string> <string name="share_selected_source">分享選中書源</string>
<string name="sort_by_lastUppdateTime">時間排序</string> <string name="sort_by_lastUppdateTime">時間排序</string>
<string name="search_content">搜索</string> <string name="search_content">全文搜索</string>
</resources> </resources>

@ -89,6 +89,7 @@
<item>页数</item> <item>页数</item>
<item>进度</item> <item>进度</item>
<item>页数及进度</item> <item>页数及进度</item>
<item>书名</item>
</string-array> </string-array>
<string-array name="text_font_weight"> <string-array name="text_font_weight">

@ -10,6 +10,8 @@
<string name="menu_import_old">导入阅读数据</string> <string name="menu_import_old">导入阅读数据</string>
<string name="mkdirs">创建子文件夹</string> <string name="mkdirs">创建子文件夹</string>
<string name="mkdirs_description">创建legado文件夹作为备份文件夹</string> <string name="mkdirs_description">创建legado文件夹作为备份文件夹</string>
<string name="webdav_cache_backup">离线缓存书籍备份</string>
<string name="webdav_cache_backup_s">导出本地同时备份到legado文件夹下exports目录</string>
<string name="backup_path">备份路径</string> <string name="backup_path">备份路径</string>
<string name="select_backup_path">请选择备份路径</string> <string name="select_backup_path">请选择备份路径</string>
<string name="menu_import_old_version">导入旧版数据</string> <string name="menu_import_old_version">导入旧版数据</string>
@ -96,6 +98,8 @@
阅读3.0下载地址:\nhttps://www.coolapk.com/apk/256030 阅读3.0下载地址:\nhttps://www.coolapk.com/apk/256030
</string> </string>
<string name="version_name">Version %s</string> <string name="version_name">Version %s</string>
<string name="pt_background_verification">后台校验书源</string>
<string name="ps_background_verification">打开后可以在校验书源时自由操作</string>
<string name="pt_auto_refresh">自动刷新</string> <string name="pt_auto_refresh">自动刷新</string>
<string name="ps_auto_refresh">打开软件时自动更新书籍</string> <string name="ps_auto_refresh">打开软件时自动更新书籍</string>
<string name="pt_auto_download">自动下载最新章节</string> <string name="pt_auto_download">自动下载最新章节</string>
@ -225,6 +229,7 @@
<string name="load_error_retry">加载失败,点击重试</string> <string name="load_error_retry">加载失败,点击重试</string>
<string name="book_intro">内容简介</string> <string name="book_intro">内容简介</string>
<string name="intro_show">简介:%s</string> <string name="intro_show">简介:%s</string>
<string name="intro_show_null">简介: 暂无简介</string>
<string name="open_from_other">打开外部书籍</string> <string name="open_from_other">打开外部书籍</string>
<string name="origin_show">来源: %s</string> <string name="origin_show">来源: %s</string>
<string name="import_replace_rule">本地导入</string> <string name="import_replace_rule">本地导入</string>
@ -290,7 +295,7 @@
<string name="padding_right">右边距</string> <string name="padding_right">右边距</string>
<string name="check_book_source">校验书源</string> <string name="check_book_source">校验书源</string>
<string name="check_select_source">校验所选</string> <string name="check_select_source">校验所选</string>
<string name="progress_show">进度 %1$d/%2$d</string> <string name="progress_show">%1$s 进度 %2$d/%3$d</string>
<string name="tts_fix">请安装并选择中文TTS!</string> <string name="tts_fix">请安装并选择中文TTS!</string>
<string name="tts_init_failed">TTS初始化失败!</string> <string name="tts_init_failed">TTS初始化失败!</string>
<string name="jf_convert">简繁转换</string> <string name="jf_convert">简繁转换</string>
@ -384,6 +389,9 @@
<string name="source_name">源名称(sourceName)</string> <string name="source_name">源名称(sourceName)</string>
<string name="source_url">源URL(sourceUrl)</string> <string name="source_url">源URL(sourceUrl)</string>
<string name="source_group">源分组(sourceGroup)</string> <string name="source_group">源分组(sourceGroup)</string>
<string name="diy_source_group">自定义源分组</string>
<string name="diy_edit_source_group">输入自定义源分组名称</string>
<string name="diy_edit_source_group_title">【%s】</string>
<string name="sort_url">分类Url</string> <string name="sort_url">分类Url</string>
<string name="login_url">登录URL(loginUrl)</string> <string name="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string> <string name="comment">源注释(sourceComment)</string>
@ -567,6 +575,7 @@
<string name="export_selection">导出所选</string> <string name="export_selection">导出所选</string>
<string name="export">导出</string> <string name="export">导出</string>
<string name="load_toc">加载目录</string> <string name="load_toc">加载目录</string>
<string name="load_info">加载详情页</string>
<string name="tts">TTS</string> <string name="tts">TTS</string>
<string name="web_dav_pw">WebDav 密码</string> <string name="web_dav_pw">WebDav 密码</string>
<string name="web_dav_pw_s">输入你的WebDav授权密码</string> <string name="web_dav_pw_s">输入你的WebDav授权密码</string>
@ -761,6 +770,6 @@
<string name="theme_list_summary">使用保存主题,导入,分享主题</string> <string name="theme_list_summary">使用保存主题,导入,分享主题</string>
<string name="select_theme">切换默认主题</string> <string name="select_theme">切换默认主题</string>
<string name="sort_by_lastUppdateTime">时间排序</string> <string name="sort_by_lastUppdateTime">时间排序</string>
<string name="search_content">搜索</string> <string name="search_content">全文搜索</string>
</resources> </resources>

@ -89,6 +89,7 @@
<item>Pages</item> <item>Pages</item>
<item>Progress</item> <item>Progress</item>
<item>Pages and progress</item> <item>Pages and progress</item>
<item>Book name</item>
</string-array> </string-array>
<string-array name="text_font_weight"> <string-array name="text_font_weight">

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="pk_background_verification" translatable="false">background_verification</string>
<string name="pk_auto_refresh" translatable="false">auto_refresh</string> <string name="pk_auto_refresh" translatable="false">auto_refresh</string>
<string name="pk_requested_direction" translatable="false">list_screen_direction</string> <string name="pk_requested_direction" translatable="false">list_screen_direction</string>
<string name="pk_full_screen" translatable="false">full_screen</string> <string name="pk_full_screen" translatable="false">full_screen</string>

@ -10,6 +10,8 @@
<string name="menu_import_old">Import Legado data</string> <string name="menu_import_old">Import Legado data</string>
<string name="mkdirs">Create a subfolder</string> <string name="mkdirs">Create a subfolder</string>
<string name="mkdirs_description">Create a folder named Legado as the backup folder.</string> <string name="mkdirs_description">Create a folder named Legado as the backup folder.</string>
<string name="webdav_cache_backup">Offline cache book backup</string>
<string name="webdav_cache_backup_s">Export to local and back up to the exports directory under the legado folder</string>
<string name="backup_path">Backup to</string> <string name="backup_path">Backup to</string>
<string name="select_backup_path">Please select a backup path.</string> <string name="select_backup_path">Please select a backup path.</string>
<string name="menu_import_old_version">Import legacy data</string> <string name="menu_import_old_version">Import legacy data</string>
@ -96,6 +98,8 @@
Legado (YueDu 3.0) download link:\n https://play.google.com/store/apps/details?id=io.legado.play.release Legado (YueDu 3.0) download link:\n https://play.google.com/store/apps/details?id=io.legado.play.release
</string> </string>
<string name="version_name">Version %s</string> <string name="version_name">Version %s</string>
<string name="pt_background_verification">Background-verification</string>
<string name="ps_background_verification">you can operate freely when verifying the book source</string>
<string name="pt_auto_refresh">Auto-refresh</string> <string name="pt_auto_refresh">Auto-refresh</string>
<string name="ps_auto_refresh">Update books automatically when opening the software</string> <string name="ps_auto_refresh">Update books automatically when opening the software</string>
<string name="pt_auto_download">Auto-download</string> <string name="pt_auto_download">Auto-download</string>
@ -225,6 +229,7 @@
<string name="load_error_retry">Load failed, tap to retry</string> <string name="load_error_retry">Load failed, tap to retry</string>
<string name="book_intro">Book description</string> <string name="book_intro">Book description</string>
<string name="intro_show">Description:%s</string> <string name="intro_show">Description:%s</string>
<string name="intro_show_null">Description: no introduction</string>
<string name="open_from_other">Open external book</string> <string name="open_from_other">Open external book</string>
<string name="origin_show">Origin: %s</string> <string name="origin_show">Origin: %s</string>
<string name="import_replace_rule">Import local rules</string> <string name="import_replace_rule">Import local rules</string>
@ -290,7 +295,7 @@
<string name="padding_right">Padding right</string> <string name="padding_right">Padding right</string>
<string name="check_book_source">Check book sources</string> <string name="check_book_source">Check book sources</string>
<string name="check_select_source">Check the selected source</string> <string name="check_select_source">Check the selected source</string>
<string name="progress_show">Progress %1$d/%2$d</string> <string name="progress_show">%1$s Progress %2$d/%3$d</string>
<string name="tts_fix">Please install and select Chinese TTS!</string> <string name="tts_fix">Please install and select Chinese TTS!</string>
<string name="tts_init_failed">TTS initialization failed!</string> <string name="tts_init_failed">TTS initialization failed!</string>
<string name="jf_convert">Simplified conversion</string> <string name="jf_convert">Simplified conversion</string>
@ -384,6 +389,9 @@
<string name="source_name">源名称(sourceName)</string> <string name="source_name">源名称(sourceName)</string>
<string name="source_url">源URL(sourceUrl)</string> <string name="source_url">源URL(sourceUrl)</string>
<string name="source_group">源分组(sourceGroup)</string> <string name="source_group">源分组(sourceGroup)</string>
<string name="diy_source_group">自定义源分组</string>
<string name="diy_edit_source_group">输入自定义源分组名称</string>
<string name="diy_edit_source_group_title">【%s】</string>
<string name="sort_url">分类Url</string> <string name="sort_url">分类Url</string>
<string name="login_url">登录URL(loginUrl)</string> <string name="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string> <string name="comment">源注释(sourceComment)</string>
@ -569,6 +577,7 @@
<string name="export_selection">Export selected</string> <string name="export_selection">Export selected</string>
<string name="export">Export</string> <string name="export">Export</string>
<string name="load_toc">Load chapters</string> <string name="load_toc">Load chapters</string>
<string name="load_info">Load book detail</string>
<string name="tts">TTS</string> <string name="tts">TTS</string>
<string name="web_dav_pw">WebDav password</string> <string name="web_dav_pw">WebDav password</string>
<string name="web_dav_pw_s">Input you WebDav authorized password</string> <string name="web_dav_pw_s">Input you WebDav authorized password</string>
@ -763,6 +772,6 @@
<string name="theme_list_summary">Save, Import, Share theme</string> <string name="theme_list_summary">Save, Import, Share theme</string>
<string name="share_selected_source">Share selected sources</string> <string name="share_selected_source">Share selected sources</string>
<string name="sort_by_lastUppdateTime">Sort by update time</string> <string name="sort_by_lastUppdateTime">Sort by update time</string>
<string name="search_content">Search</string> <string name="search_content">Search content</string>
</resources> </resources>

@ -36,6 +36,15 @@
app:allowDividerBelow="false" app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.SwitchPreference
android:key="webDavExport"
android:defaultValue="false"
android:title="@string/webdav_cache_backup"
android:summary="@string/webdav_cache_backup_s"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" />
</io.legado.app.ui.widget.prefs.PreferenceCategory> </io.legado.app.ui.widget.prefs.PreferenceCategory>
<io.legado.app.ui.widget.prefs.PreferenceCategory <io.legado.app.ui.widget.prefs.PreferenceCategory

@ -51,6 +51,13 @@
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:layout="@layout/view_preference_category"> app:layout="@layout/view_preference_category">
<io.legado.app.ui.widget.prefs.SwitchPreference
android:defaultValue="false"
android:key="@string/pk_background_verification"
android:summary="@string/ps_background_verification"
android:title="@string/pt_background_verification"
app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.SwitchPreference <io.legado.app.ui.widget.prefs.SwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="replaceEnableDefault" android:key="replaceEnableDefault"

Loading…
Cancel
Save