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)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**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**
* 修复导入排版字体重复报错的bug
* 添加正文搜索 by [h11128](https://github.com/h11128)

@ -7,10 +7,7 @@ import android.content.ContentProvider
import android.content.ContentResolver
import android.content.ContentValues
import android.content.UriMatcher
import android.database.CharArrayBuffer
import android.database.ContentObserver
import android.database.Cursor
import android.database.DataSetObserver
import android.database.*
import android.net.Uri
import android.os.Bundle
import com.google.gson.Gson
@ -109,40 +106,22 @@ class ReaderProvider : ContentProvider() {
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
) = throw UnsupportedOperationException("Not yet implemented")
/**
* Simple inner class to deliver json callback data.
*
* 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)
override fun getCount() = 1
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
init {
addRow(arrayOf(mData))
}
override fun isAfterLast() = true
override fun getCount() = 1
override fun getColumnIndex(s: String) = 0

@ -20,4 +20,8 @@ object EventBus {
const val WEB_SERVICE_STOP = "webServiceStop"
const val UP_DOWNLOAD = "upDownload"
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 webDavPassword = "web_dav_password"
const val webDavCreateDir = "webDavCreateDir"
const val webDavCacheBackup = "webDavCacheBackup"
const val changeSourceLoadToc = "changeSourceLoadToc"
const val changeSourceLoadInfo = "changeSourceLoadInfo"
const val chineseConverterType = "chineseConverterType"
const val launcherIcon = "launcherIcon"
const val textSelectAble = "selectText"

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

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

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

@ -54,6 +54,7 @@ object Backup {
context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis())
withContext(IO) {
synchronized(this@Backup) {
FileUtils.deleteFile(backupPath)
writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath)
writeListToJson(App.db.bookmarkDao().all, "bookmark.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.webdav.WebDav
import io.legado.app.lib.webdav.http.HttpAuth
import io.legado.app.utils.FileUtils
import io.legado.app.utils.ZipUtils
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
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
for (sourceRule in ruleList) {
putRule(sourceRule.putMap)
sourceRule.makeUpRule(result)
result?.let {
result = when (sourceRule.mode) {
Mode.Regex -> AnalyzeByRegex.getElement(
@ -324,37 +325,17 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
*/
private fun replaceRegex(result: String, rule: SourceRule): String {
var vResult = result
val stringBuffer = StringBuffer()
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()) {
if (rule.replaceRegex.isNotEmpty()) {
vResult = if (rule.replaceFirst) {
val pattern = Pattern.compile(replaceRegex)
val pattern = Pattern.compile(rule.replaceRegex)
val matcher = pattern.matcher(vResult)
if (matcher.find()) {
matcher.group(0)!!.replaceFirst(replaceRegex.toRegex(), rule.replacement)
matcher.group(0)!!.replaceFirst(rule.replaceRegex.toRegex(), rule.replacement)
} else {
""
}
} else {
vResult.replace(replaceRegex.toRegex(), rule.replacement)
vResult.replace(rule.replaceRegex.toRegex(), rule.replacement)
}
}
return vResult
@ -425,6 +406,9 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
internal val putMap = HashMap<String, String>()
private val ruleParam = ArrayList<String>()
private val ruleType = ArrayList<Int>()
private val getRuleType = -2
private val jsRuleType = -1
private val defaultRuleType = 0
init {
this.mode = mainMode
@ -465,68 +449,71 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
}
//分离put
rule = splitPutRule(rule, putMap)
//分离正则表达式
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, 拆分
//@get,{{ }}, 拆分
var start = 0
var tmp: String
val evalMatcher = evalPattern.matcher(rule)
while (evalMatcher.find()) {
if (mode != Mode.Js) {
mode = Mode.Regex
}
if (evalMatcher.start() > start) {
tmp = rule.substring(start, evalMatcher.start())
ruleType.add(0)
ruleParam.add(tmp)
if (start == 0 && !tmp.contains("##") && mode != Mode.Js) {
mode = Mode.Regex
}
splitRegex(tmp)
}
tmp = evalMatcher.group()
when {
tmp.startsWith("$") -> {
ruleType.add(tmp.substring(1).toInt())
ruleParam.add(tmp)
}
tmp.startsWith("@get:", true) -> {
ruleType.add(-2)
ruleType.add(getRuleType)
ruleParam.add(tmp.substring(6, tmp.lastIndex))
}
tmp.startsWith("{{") -> {
ruleType.add(-1)
ruleType.add(jsRuleType)
ruleParam.add(tmp.substring(2, tmp.length - 2))
}
else -> {
ruleType.add(0)
ruleParam.add(tmp)
splitRegex(tmp)
}
}
start = evalMatcher.end()
}
if (rule.length > 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)
}
}
/**
* 替换@get,{{ }},$1,
* 替换@get,{{ }}
*/
fun makeUpRule(result: Any?) {
val infoVal = StringBuilder()
@ -535,7 +522,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
while (index-- > 0) {
val regType = ruleType[index]
when {
regType > 0 -> {
regType > defaultRuleType -> {
@Suppress("UNCHECKED_CAST")
val resultList = result as? List<String?>
if (resultList != null) {
@ -548,7 +535,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
infoVal.insert(0, ruleParam[index])
}
}
regType == -1 -> {
regType == jsRuleType -> {
if (isRule(ruleParam[index])) {
getString(arrayListOf(SourceRule(ruleParam[index]))).let {
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]))
}
else -> infoVal.insert(0, ruleParam[index])
@ -574,6 +561,18 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
}
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 {
@ -660,14 +659,9 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
companion object {
private val putPattern = Pattern.compile("@put:(\\{[^}]+?\\})", Pattern.CASE_INSENSITIVE)
private val getPattern = Pattern.compile("@get:\\{([^}]+?)\\}", Pattern.CASE_INSENSITIVE)
private val evalPattern = Pattern.compile(
"@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}|\\$\\d{1,2}",
Pattern.CASE_INSENSITIVE
)
private val replacePattern = Pattern.compile(
"@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}",
Pattern.CASE_INSENSITIVE
)
private val evalPattern =
Pattern.compile("@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}", Pattern.CASE_INSENSITIVE)
private val regexPattern = Pattern.compile("\\$\\d{1,2}")
}
}

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

@ -368,7 +368,19 @@ object ReadBook {
val currentLine = currentPage.textLines[lineIndex]
length -= currentLine.text.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 关于界面
* association 导入书源界面
* audio 音频播放界面
* book\arrange 书架整理界面
* book\info 书籍信息查看

@ -20,16 +20,21 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.help.IntentDataHelp
import io.legado.app.help.SourceHelp
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.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
import io.legado.app.utils.visible
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.item_source_import.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.toast
class ImportBookSourceActivity : VMBaseActivity<ImportBookSourceViewModel>(
R.layout.activity_translucence,
theme = Theme.Transparent
@ -112,6 +117,7 @@ class ImportBookSourceActivity : VMBaseActivity<ImportBookSourceViewModel>(
class SourcesDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
lateinit var adapter: SourcesAdapter
private var _groupName: String? = null
override fun onStart() {
super.onStart()
@ -165,6 +171,23 @@ class ImportBookSourceActivity : VMBaseActivity<ImportBookSourceViewModel>(
override fun onMenuItemClick(item: MenuItem): Boolean {
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 -> {
adapter.selectStatus.forEachIndexed { index, b ->
if (!b) {
@ -193,6 +216,9 @@ class ImportBookSourceActivity : VMBaseActivity<ImportBookSourceViewModel>(
private fun importSelect() {
val selectSource = arrayListOf<BookSource>()
adapter.selectStatus.forEachIndexed { index, b ->
if (_groupName != null) {
adapter.getItem(index)!!.bookSourceGroup = _groupName
}
if (b) {
selectSource.add(adapter.getItem(index)!!)
}

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

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

@ -120,11 +120,11 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
.onSuccess(IO) {
it.forEach { searchBook ->
if (searchBook.name == name && searchBook.author == author) {
if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
if (searchBook.tocUrl.isEmpty()) {
if (searchBook.latestChapterTitle.isNullOrEmpty()) {
if (context.getPrefBoolean(PreferKey.changeSourceLoadInfo) || context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
loadBookInfo(searchBook.toBook())
} else {
loadChapter(searchBook.toBook())
searchFinish(searchBook)
}
} else {
searchFinish(searchBook)
@ -156,7 +156,14 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this)
.onSuccess {
loadChapter(it)
if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) {
loadChapter(it)
} else {
//从详情页里获取最新章节
book.latestChapterTitle = it.latestChapterTitle
val searchBook = book.toSearchBook()
searchFinish(searchBook)
}
}.onError {
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.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()
if (kinds.isEmpty()) {
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.delegate.PageDelegate
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.login.SourceLogin
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 kotlinx.android.synthetic.main.activity_book_read.*
import kotlinx.android.synthetic.main.view_read_menu.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
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.startActivity
import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.toast
import java.lang.Runnable
class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_book_read),
View.OnTouchListener,
@ -496,6 +493,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
)
return true
}
R.id.menu_search_content -> {
openSearchActivity(selectedText)
return true
}
}
return false
}
@ -680,12 +681,12 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
/**
* 打开搜索界面
*/
//todo: change request code
override fun openSearchList() {
override fun openSearchActivity(searchWord: String?) {
ReadBook.book?.let {
startActivityForResult<SearchListActivity>(
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
}
val positions = ReadBook.searchResultPositions(pages, indexWithinChapter, query!!)
//todo: show selected text
val job1 = async(Main){
while (ReadBook.durPageIndex != positions[0]){
delay(100L)
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
delay(100L)
}
job1.await()
}
}
requestCodeReplace -> onReplaceRuleSave()

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

@ -11,6 +11,7 @@ import io.legado.app.base.BaseActivity
import io.legado.app.constant.AppConst.timeFormat
import io.legado.app.help.ReadBookConfig
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.provider.ChapterProvider
import io.legado.app.ui.widget.BatteryView
@ -29,6 +30,7 @@ class ContentView(context: Context) : FrameLayout(context) {
private var tvPage: BatteryView? = null
private var tvTotalProgress: BatteryView? = null
private var tvPageAndTotal: BatteryView? = null
private var tvBookName: BatteryView? = null
val headerHeight: Int
get() = if (ReadBookConfig.hideStatusBar) {
@ -185,6 +187,19 @@ class ContentView(context: Context) : FrameLayout(context) {
isBattery = false
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?) {
@ -213,8 +228,8 @@ class ContentView(context: Context) : FrameLayout(context) {
@SuppressLint("SetTextI18n")
fun setProgress(textPage: TextPage) = textPage.apply {
val title = textPage.title
tvTitle?.text = title
tvBookName?.text = ReadBook.book?.name
tvTitle?.text = textPage.title
tvPage?.text = "${index.plus(1)}/$pageSize"
tvTotalProgress?.text = 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)
bv_originCount.setBadgeCount(searchBook.origins.size)
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())
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)
"origins" -> bv_originCount.setBadgeCount(searchBook.origins.size)
"last" -> upLasted(itemView, searchBook.latestChapterTitle)
"intro" -> tv_introduce.text =
context.getString(R.string.intro_show, searchBook.intro)
"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())
"cover" -> iv_cover.load(
searchBook.coverUrl,

@ -1,93 +1,264 @@
package io.legado.app.ui.book.searchContent
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import androidx.appcompat.widget.SearchView
import androidx.core.view.isGone
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import com.hankcs.hanlp.HanLP
import io.legado.app.App
import io.legado.app.R
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.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.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.gone
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.activity_chapter_list.*
import kotlinx.android.synthetic.main.view_tab_layout.*
import io.legado.app.utils.observeEvent
import kotlinx.android.synthetic.main.activity_search_list.*
import kotlinx.android.synthetic.main.view_search.*
import kotlinx.coroutines.*
import org.jetbrains.anko.sdk27.listeners.onClick
class SearchListActivity : VMBaseActivity<SearchListViewModel>(R.layout.activity_search_list) {
// todo: 完善搜索界面UI
class SearchListActivity : VMBaseActivity<SearchListViewModel>(R.layout.activity_search_list),
SearchListAdapter.Callback {
override val viewModel: SearchListViewModel
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?) {
tab_layout.isTabIndicatorFullWidth = false
tab_layout.setSelectedTabIndicatorColor(accentColor)
val bbg = bottomBackground
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 {
viewModel.initBook(it) {
view_pager.adapter = TabFragmentPageAdapter(supportFragmentManager)
tab_layout.setupWithViewPager(view_pager)
initBook()
}
}
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.search_view, menu)
val search = menu.findItem(R.id.menu_search)
searchView = search.actionView as SearchView
ATH.setTint(searchView!!, primaryTextColor)
searchView?.maxWidth = resources.displayMetrics.widthPixels
searchView?.onActionViewCollapsed()
searchView?.setOnCloseListener {
tab_layout.visible()
//to do clean
false
}
searchView?.setOnSearchClickListener { tab_layout.gone() }
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
private fun initSearchView() {
ATH.setTint(search_view, primaryTextColor)
search_view.onActionViewExpanded()
search_view.isSubmitButtonEnabled = true
search_view.queryHint = getString(R.string.search)
search_view.clearFocus()
search_view.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
if (viewModel.lastQuery != query){
viewModel.startContentSearch(query)
if (viewModel.lastQuery != query) {
startContentSearch(query)
}
return false
}
override fun onQueryTextChange(newText: String): Boolean {
override fun onQueryTextChange(newText: String?): Boolean {
return false
}
})
return super.onCompatCreateOptionsMenu(menu)
}
private inner class TabFragmentPageAdapter(fm: FragmentManager) :
FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItem(position: Int): Fragment {
return SearchListFragment()
private fun initRecyclerView() {
adapter = SearchListAdapter(this, this)
mLayoutManager = UpLinearLayoutManager(this)
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 {
return 1
@SuppressLint("SetTextI18n")
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? {
return "Search"
private suspend fun searchChapter(query: String, chapter: BookChapter?): List<SearchResult> {
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() {
if (tab_layout.isGone) {
searchView?.onActionViewCollapsed()
tab_layout.visible()
} else {
super.onBackPressed()
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)
setResult(RESULT_OK, searchData)
finish()
}
override fun durChapterIndex(): Int {
return durChapterIndex
}
}

@ -1,21 +1,12 @@
package io.legado.app.ui.book.searchContent
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.base.adapter.ItemViewHolder
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.utils.getCompatColor
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_bookmark.view.*
import io.legado.app.utils.hexString
import kotlinx.android.synthetic.main.item_search_list.view.*
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) {
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>) {
with(holder.itemView) {
val isDur = callback.durChapterIndex() == item.chapterIndex
if (payloads.isEmpty()) {
tv_search_result.text = item.parseText(item.presentText)
if (isDur){
tv_search_result.paint.isFakeBoldText = true
}
tv_search_result.text = item.getHtmlCompat(textColor, accentColor)
tv_search_result.paint.isFakeBoldText = isDur
}
}
}

@ -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) {
var bookUrl: String = ""
var book: Book? = null
var searchCallBack: SearchListCallBack? = null
var lastQuery: String = ""
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 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(
var index: Int = 0,
@ -19,25 +15,25 @@ data class SearchResult(
var newPosition: 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 sub2 = text.substring(position + center.length, targetText.length)
val textColor = App.INSTANCE.getCompatColor(R.color.primaryText).hexString
return "<font color=#${textColor}>$sub1</font>" +
"<font color=#ff0000>$center</font>" +
"<font color=#${accentColor}>$center</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.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.SubMenu
import android.util.DisplayMetrics
import android.view.*
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.core.content.FileProvider
@ -19,8 +18,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.BuildConfig
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppPattern
import io.legado.app.constant.EventBus
import io.legado.app.data.entities.BookSource
import io.legado.app.help.IntentDataHelp
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.utils.*
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_progressbar_view.*
import kotlinx.android.synthetic.main.dialog_progressbar_view.tv_footer_left
import kotlinx.android.synthetic.main.view_search.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.toast
@ -384,6 +389,24 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
}.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() {
select_action_bar.upCountView(adapter.getSelection().size, adapter.getActualItemCount())
}
@ -478,4 +501,47 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
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.help.coroutine.Coroutine
import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.ACache
import io.legado.app.utils.dp
import io.legado.app.utils.gone
import io.legado.app.utils.visible
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.item_fillet_text.view.*
import kotlinx.android.synthetic.main.item_find_book.view.*
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)
gl_child.addView(tv)
tv.text_view.text = kind.title
tv.text_view.onClick {
kind.url?.let { kindUrl ->
if (!kind.url.isNullOrEmpty()) {
tv.text_view.onClick {
callBack.openExplore(
item.bookSourceUrl,
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) {
execute {
val fontItems = arrayListOf<DocItem>()
@ -132,6 +152,7 @@ class FontSelectDialog : BaseDialogFragment(),
fontItems.add(item)
}
}
fontItems.addAll(getLocalFonts())
fontItems.sortedBy { it.name }
}.onSuccess {
adapter?.setItems(it)
@ -167,6 +188,7 @@ class FontSelectDialog : BaseDialogFragment(),
)
)
}
fontItems.addAll(getLocalFonts())
fontItems.sortedBy { it.name }
}.onSuccess {
adapter?.setItems(it)

@ -1,19 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<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">
<io.legado.app.ui.widget.TitleBar
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentLayout="@layout/view_tab_layout"/>
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentLayout="@layout/view_search"
app:contentInsetRight="24dp"
app:layout_constraintTop_toTopOf="parent" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<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_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: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
android:id="@+id/fabAutoPage"
android:layout_width="wrap_content"
@ -265,44 +285,6 @@
android:layout_height="match_parent"
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
android:id="@+id/ll_read_aloud"

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

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

@ -3,6 +3,12 @@
xmlns:android="http://schemas.android.com/apk/res/android"
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
android:id="@+id/menu_select_all"
android:title="@string/select_all"

@ -35,6 +35,17 @@
<item>系統等寬字體</item>
</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">
<item>正常</item>
<item>粗體</item>

@ -10,6 +10,8 @@
<string name="menu_import_old">導入閲讀數據</string>
<string name="mkdirs">創建子文件夾</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="menu_import_old_version">導入舊版數據</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
</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="ps_auto_refresh">打開程式時自動更新書輯</string>
<string name="pt_auto_download">自動下載最新章節</string>
@ -223,6 +227,7 @@
<string name="load_error_retry">載入失敗,點擊重試</string>
<string name="book_intro">內容簡介</string>
<string name="intro_show">簡介: %s</string>
<string name="intro_show_null">簡介: 暫無簡介</string>
<string name="open_from_other">打開外部書籍</string>
<string name="origin_show">來源: %s</string>
<string name="import_replace_rule">本地導入</string>
@ -288,7 +293,7 @@
<string name="padding_right">右邊距</string>
<string name="check_book_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_init_failed">TTS 初始化失敗!</string>
<string name="jf_convert">簡繁轉換</string>
@ -563,6 +568,7 @@
<string name="export_selection">導出所選</string>
<string name="export">導出</string>
<string name="load_toc">加載目錄</string>
<string name="load_info">加載詳情頁</string>
<string name="tts">TTS</string>
<string name="web_dav_pw">WebDav 密碼</string>
<string name="web_dav_pw_s">輸入你的 WebDav 授權密碼</string>
@ -761,6 +767,6 @@
<string name="select_theme">切換默認主題</string>
<string name="share_selected_source">分享選中書源</string>
<string name="sort_by_lastUppdateTime">時間排序</string>
<string name="search_content">搜索</string>
<string name="search_content">全文搜索</string>
</resources>

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

@ -10,6 +10,8 @@
<string name="menu_import_old">匯入閱讀資料</string>
<string name="mkdirs">建立子資料夾</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="menu_import_old_version">匯入舊版資料</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
</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="ps_auto_refresh">打開軟體時自動更新書籍</string>
<string name="pt_auto_download">自動下載最新章節</string>
@ -223,6 +227,7 @@
<string name="load_error_retry">載入失敗,點擊重試</string>
<string name="book_intro">內容簡介</string>
<string name="intro_show">簡介:%s</string>
<string name="intro_show_null">簡介: 暫無簡介</string>
<string name="open_from_other">打開外部書籍</string>
<string name="origin_show">來源: %s</string>
<string name="import_replace_rule">本機匯入</string>
@ -288,7 +293,7 @@
<string name="padding_right">右邊距</string>
<string name="check_book_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_init_failed">TTS初始化失敗!</string>
<string name="jf_convert">簡繁轉換</string>
@ -564,6 +569,7 @@
<string name="export_selection">匯出所選</string>
<string name="export">匯出</string>
<string name="load_toc">載入目錄</string>
<string name="load_info">載入詳情頁</string>
<string name="tts">TTS</string>
<string name="web_dav_pw">WebDav 密碼</string>
<string name="web_dav_pw_s">輸入你的WebDav授權密碼</string>
@ -761,7 +767,6 @@
<string name="select_theme">切換默認主題</string>
<string name="share_selected_source">分享選中書源</string>
<string name="sort_by_lastUppdateTime">時間排序</string>
<string name="search_content">搜索</string>
<string name="search_content">全文搜索</string>
</resources>

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

@ -10,6 +10,8 @@
<string name="menu_import_old">导入阅读数据</string>
<string name="mkdirs">创建子文件夹</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="select_backup_path">请选择备份路径</string>
<string name="menu_import_old_version">导入旧版数据</string>
@ -96,6 +98,8 @@
阅读3.0下载地址:\nhttps://www.coolapk.com/apk/256030
</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="ps_auto_refresh">打开软件时自动更新书籍</string>
<string name="pt_auto_download">自动下载最新章节</string>
@ -225,6 +229,7 @@
<string name="load_error_retry">加载失败,点击重试</string>
<string name="book_intro">内容简介</string>
<string name="intro_show">简介:%s</string>
<string name="intro_show_null">简介: 暂无简介</string>
<string name="open_from_other">打开外部书籍</string>
<string name="origin_show">来源: %s</string>
<string name="import_replace_rule">本地导入</string>
@ -290,7 +295,7 @@
<string name="padding_right">右边距</string>
<string name="check_book_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_init_failed">TTS初始化失败!</string>
<string name="jf_convert">简繁转换</string>
@ -384,6 +389,9 @@
<string name="source_name">源名称(sourceName)</string>
<string name="source_url">源URL(sourceUrl)</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="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string>
@ -567,6 +575,7 @@
<string name="export_selection">导出所选</string>
<string name="export">导出</string>
<string name="load_toc">加载目录</string>
<string name="load_info">加载详情页</string>
<string name="tts">TTS</string>
<string name="web_dav_pw">WebDav 密码</string>
<string name="web_dav_pw_s">输入你的WebDav授权密码</string>
@ -761,6 +770,6 @@
<string name="theme_list_summary">使用保存主题,导入,分享主题</string>
<string name="select_theme">切换默认主题</string>
<string name="sort_by_lastUppdateTime">时间排序</string>
<string name="search_content">搜索</string>
<string name="search_content">全文搜索</string>
</resources>

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

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pk_background_verification" translatable="false">background_verification</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_full_screen" translatable="false">full_screen</string>

@ -10,6 +10,8 @@
<string name="menu_import_old">Import Legado data</string>
<string name="mkdirs">Create a subfolder</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="select_backup_path">Please select a backup path.</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
</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="ps_auto_refresh">Update books automatically when opening the software</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="book_intro">Book description</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="origin_show">Origin: %s</string>
<string name="import_replace_rule">Import local rules</string>
@ -290,7 +295,7 @@
<string name="padding_right">Padding right</string>
<string name="check_book_source">Check book sources</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_init_failed">TTS initialization failed!</string>
<string name="jf_convert">Simplified conversion</string>
@ -384,6 +389,9 @@
<string name="source_name">源名称(sourceName)</string>
<string name="source_url">源URL(sourceUrl)</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="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string>
@ -569,6 +577,7 @@
<string name="export_selection">Export selected</string>
<string name="export">Export</string>
<string name="load_toc">Load chapters</string>
<string name="load_info">Load book detail</string>
<string name="tts">TTS</string>
<string name="web_dav_pw">WebDav 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="share_selected_source">Share selected sources</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>

@ -36,6 +36,15 @@
app:allowDividerBelow="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

@ -51,6 +51,13 @@
app:iconSpaceReserved="false"
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
android:defaultValue="true"
android:key="replaceEnableDefault"

Loading…
Cancel
Save