Merge remote-tracking branch 'upstream/master' into h11128dev

pull/393/head
Jason Yao 4 years ago
commit 9559fcf131
  1. 19
      app/src/main/assets/updateLog.md
  2. 33
      app/src/main/java/io/legado/app/api/ReaderProvider.kt
  3. 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  4. 2
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  5. 1
      app/src/main/java/io/legado/app/help/ReadTipConfig.kt
  6. 1
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  7. 25
      app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt
  8. 65
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  9. 1
      app/src/main/java/io/legado/app/ui/README.md
  10. 26
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceActivity.kt
  11. 25
      app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt
  12. 12
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  13. 4
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  14. 19
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  15. 271
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListActivity.kt
  16. 243
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListFragment.kt
  17. 10
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListViewModel.kt
  18. 22
      app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt
  19. 69
      app/src/main/res/layout/activity_search_list.xml
  20. 66
      app/src/main/res/layout/fragment_search_list.xml
  21. 4
      app/src/main/res/menu/content_select_action.xml
  22. 6
      app/src/main/res/menu/import_source.xml
  23. 11
      app/src/main/res/values-zh-rHK/arrays.xml
  24. 4
      app/src/main/res/values-zh-rHK/strings.xml
  25. 1
      app/src/main/res/values-zh-rTW/arrays.xml
  26. 5
      app/src/main/res/values-zh-rTW/strings.xml
  27. 1
      app/src/main/res/values-zh/arrays.xml
  28. 7
      app/src/main/res/values-zh/strings.xml
  29. 1
      app/src/main/res/values/arrays.xml
  30. 7
      app/src/main/res/values/strings.xml
  31. 9
      app/src/main/res/xml/pref_config_backup.xml

@ -3,6 +3,25 @@
* 关注合作公众号 **[小说拾遗]()** 获取好看的小说。
- 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**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
@ -116,33 +113,15 @@ class ReaderProvider : ContentProvider() {
*
* 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

@ -33,6 +33,7 @@ object PreferKey {
const val webDavAccount = "web_dav_account"
const val webDavPassword = "web_dav_password"
const val webDavCreateDir = "webDavCreateDir"
const val webDavExport = "webDavExport"
const val changeSourceLoadToc = "changeSourceLoadToc"
const val chineseConverterType = "chineseConverterType"
const val launcherIcon = "launcherIcon"

@ -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
@ -465,33 +446,11 @@ 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, 拆分
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)
@ -499,7 +458,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
}
tmp = evalMatcher.group()
when {
tmp.startsWith("$") -> {
tmp.startsWith("$") && !rule.contains("##") -> {
ruleType.add(tmp.substring(1).toInt())
ruleParam.add(tmp)
}
@ -574,6 +533,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 {

@ -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,21 @@ 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.webDavExport,false)) {
//写出文件到cache目录
FileUtils.createFileIfNotExist(
File(FileUtils.getCachePath()),
filename
).writeText(content)
//导出到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 +76,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.webDavExport,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 ->

@ -9,7 +9,6 @@ import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.*
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.core.view.get
@ -49,7 +48,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
@ -496,6 +494,10 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
)
return true
}
R.id.menu_search_content -> {
openSearchActivity(selectedText)
return true
}
}
return false
}
@ -680,12 +682,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)
)
}
}

@ -175,7 +175,7 @@ class ReadMenu : FrameLayout {
//搜索
fabSearch.onClick {
runMenuOut {
callBack?.openSearchList()
callBack?.openSearchActivity(null)
}
}
@ -300,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"

@ -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
}
override fun getCount(): Int {
return 1
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)
}
}
}
override fun getPageTitle(position: Int): CharSequence? {
return "Search"
@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 onBackPressed() {
if (tab_layout.isGone) {
searchView?.onActionViewCollapsed()
tab_layout.visible()
} else {
super.onBackPressed()
@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()
}
}
}
}
}
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
}
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,243 +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)
//Log.d("h11128", "find ${searchResults.size} results in chapter ${chapter.title}")
}
}
job.await()
if (searchResults.isNotEmpty()){
//Log.d("h11128", "load ${searchResults.size} results in chapter ${chapter.title}")
searchResultList.addAll(searchResults)
tv_current_search_info.text = "搜索结果:$searchResultCounts"
//Log.d("h11128", "searchResultList size ${searchResultList.size}")
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)
Log.d("h11128","current chapter index ${searchResult.chapterIndex}")
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)
}
}

@ -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,7 +1,7 @@
<?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"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -9,11 +9,66 @@
android:id="@+id/title_bar"
android:layout_width="match_parent"
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
android:id="@+id/view_pager"
<io.legado.app.ui.widget.recycler.scroller.FastScrollRecyclerView
android:id="@+id/recycler_view"
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>

@ -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>

@ -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="export_webdav">離線導出WebDav</string>
<string name="export_webdav_s">默認導出到legado文件夾下exports目錄</string>
<string name="backup_path">備份路徑</string>
<string name="menu_import_old_version">導入舊版數據</string>
<string name="menu_import_github">導入 Github 數據</string>
@ -764,6 +766,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="export_webdav">離線導出WebDav</string>
<string name="export_webdav_s">默認導出到legado文件夾下exports目錄</string>
<string name="backup_path">備份路徑</string>
<string name="menu_import_old_version">匯入舊版資料</string>
<string name="menu_import_github">匯入Github資料</string>
@ -764,7 +766,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="export_webdav">离线导出WebDav</string>
<string name="export_webdav_s">默认导出到legado文件夹下exports目录</string>
<string name="backup_path">备份路径</string>
<string name="select_backup_path">请选择备份路径</string>
<string name="menu_import_old_version">导入旧版数据</string>
@ -387,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>
@ -764,6 +769,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">

@ -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="export_webdav">Export Webdav</string>
<string name="export_webdav_s">Default export 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>
@ -387,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>
@ -766,6 +771,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/export_webdav"
android:summary="@string/export_webdav_s"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" />
</io.legado.app.ui.widget.prefs.PreferenceCategory>
<io.legado.app.ui.widget.prefs.PreferenceCategory

Loading…
Cancel
Save