搜索菜单 (#1)

* 左右跳转搜索结果

已实现,跳转index有bug

* some progress

1. fix navigate to first search result stuck
2.  add image for navigation arrow
3. name change for potential usage of search result

* 左右跳转搜索结果

已实现,跳转index有bug

* some progress

1. fix navigate to first search result stuck
2.  add image for navigation arrow
3. name change for potential usage of search result

* update

* 搜索优化

* 左右跳转搜索结果

已实现,跳转index有bug

* some progress

1. fix navigate to first search result stuck
2.  add image for navigation arrow
3. name change for potential usage of search result

* 搜索优化

* 左右跳转搜索结果

已实现,跳转index有bug

* some progress

1. fix navigate to first search result stuck
2.  add image for navigation arrow
3. name change for potential usage of search result

* 搜索优化

* delete title bar

delete title bar
fix animation no working after deletion
add temp setting entry

* 更新选中文字颜色

* comment out code related to setting

* 左右跳转搜索结果

已实现,跳转index有bug

* some progress

1. fix navigate to first search result stuck
2.  add image for navigation arrow
3. name change for potential usage of search result

* 搜索优化

* delete title bar

delete title bar
fix animation no working after deletion
add temp setting entry

* 更新选中文字颜色

* comment out code related to setting

* revert some change after review

* revert some change after review

* revert some change after review
pull/1463/head
Jason Yao 3 years ago committed by GitHub
parent 238f341015
commit adaae819d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/src/main/java/io/legado/app/constant/EventBus.kt
  2. 72
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  3. 18
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  4. 235
      app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt
  5. 6
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  6. 3
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt
  7. 122
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt
  8. 80
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt
  9. 41
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt
  10. 6
      app/src/main/res/layout/activity_book_read.xml
  11. 276
      app/src/main/res/layout/view_search_menu.xml
  12. 1
      app/src/main/res/values-es-rES/strings.xml
  13. 1
      app/src/main/res/values-ja-rJP/strings.xml
  14. 1
      app/src/main/res/values-pt-rBR/strings.xml
  15. 1
      app/src/main/res/values-zh-rHK/strings.xml
  16. 1
      app/src/main/res/values-zh-rTW/strings.xml
  17. 1
      app/src/main/res/values-zh/strings.xml
  18. 1
      app/src/main/res/values/strings.xml

@ -27,4 +27,5 @@ object EventBus {
const val CHECK_SOURCE_DONE = "checkSourceDone" const val CHECK_SOURCE_DONE = "checkSourceDone"
const val TIP_COLOR = "tipColor" const val TIP_COLOR = "tipColor"
const val SOURCE_CHANGED = "sourceChanged" const val SOURCE_CHANGED = "sourceChanged"
const val SEARCH_RESULT = "searchResult"
} }

@ -46,6 +46,7 @@ import io.legado.app.ui.book.read.page.ReadView
import io.legado.app.ui.book.read.page.entities.PageDirection import io.legado.app.ui.book.read.page.entities.PageDirection
import io.legado.app.ui.book.read.page.provider.TextPageFactory import io.legado.app.ui.book.read.page.provider.TextPageFactory
import io.legado.app.ui.book.searchContent.SearchContentActivity import io.legado.app.ui.book.searchContent.SearchContentActivity
import io.legado.app.ui.book.searchContent.SearchResult
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.toc.BookmarkDialog import io.legado.app.ui.book.toc.BookmarkDialog
import io.legado.app.ui.book.toc.TocActivityResult import io.legado.app.ui.book.toc.TocActivityResult
@ -65,6 +66,7 @@ class ReadBookActivity : BaseReadBookActivity(),
TextActionMenu.CallBack, TextActionMenu.CallBack,
ContentTextView.CallBack, ContentTextView.CallBack,
ReadMenu.CallBack, ReadMenu.CallBack,
SearchMenu.CallBack,
ReadAloudDialog.CallBack, ReadAloudDialog.CallBack,
ChangeSourceDialog.CallBack, ChangeSourceDialog.CallBack,
ReadBook.CallBack, ReadBook.CallBack,
@ -93,14 +95,18 @@ class ReadBookActivity : BaseReadBookActivity(),
viewModel.replaceRuleChanged() viewModel.replaceRuleChanged()
} }
} }
private val searchContentActivity = private val searchContentActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
it ?: return@registerForActivityResult it ?: return@registerForActivityResult
it.data?.let { data -> it.data?.let { data ->
data.getIntExtra("index", ReadBook.durChapterIndex).let { index -> data.getIntExtra("chapterIndex", ReadBook.durChapterIndex).let { _ ->
viewModel.searchContentQuery = data.getStringExtra("query") ?: "" viewModel.searchContentQuery = data.getStringExtra("query") ?: ""
val indexWithinChapter = data.getIntExtra("indexWithinChapter", 0) val searchResultIndex = data.getIntExtra("searchResultIndex", 0)
skipToSearch(index, indexWithinChapter) isShowingSearchResult = true
binding.searchMenu.updateSearchResultIndex(searchResultIndex)
binding.searchMenu.selectedSearchResult?.let { currentResult ->
skipToSearch(currentResult)
showActionMenu()
}
} }
} }
} }
@ -116,6 +122,11 @@ class ReadBookActivity : BaseReadBookActivity(),
private var backupJob: Job? = null private var backupJob: Job? = null
override var autoPageProgress = 0 override var autoPageProgress = 0
override var isAutoPage = false override var isAutoPage = false
override var isShowingSearchResult = false
override var isSelectingSearchResult = false
set(value) {
field = value && isShowingSearchResult
}
private var screenTimeOut: Long = 0 private var screenTimeOut: Long = 0
private var timeBatteryReceiver: TimeBatteryReceiver? = null private var timeBatteryReceiver: TimeBatteryReceiver? = null
private var loadStates: Boolean = false private var loadStates: Boolean = false
@ -634,6 +645,7 @@ class ReadBookActivity : BaseReadBookActivity(),
when { when {
BaseReadAloudService.isRun -> showReadAloudDialog() BaseReadAloudService.isRun -> showReadAloudDialog()
isAutoPage -> showDialogFragment<AutoReadDialog>() isAutoPage -> showDialogFragment<AutoReadDialog>()
isShowingSearchResult -> binding.searchMenu.runMenuIn()
else -> binding.readMenu.runMenuIn() else -> binding.readMenu.runMenuIn()
} }
} }
@ -765,6 +777,10 @@ class ReadBookActivity : BaseReadBookActivity(),
showDialogFragment<MoreConfigDialog>() showDialogFragment<MoreConfigDialog>()
} }
override fun showSearchSetting() {
showDialogFragment<MoreConfigDialog>()
}
/** /**
* 更新状态栏,导航栏 * 更新状态栏,导航栏
*/ */
@ -773,6 +789,13 @@ class ReadBookActivity : BaseReadBookActivity(),
upNavigationBarColor() upNavigationBarColor()
} }
override fun exitSearchMenu() {
if (isShowingSearchResult) {
isShowingSearchResult = false
binding.searchMenu.invalidate()
}
}
override fun showLogin() { override fun showLogin() {
ReadBook.bookSource?.let { ReadBook.bookSource?.let {
startActivity<SourceLoginActivity> { startActivity<SourceLoginActivity> {
@ -869,34 +892,45 @@ class ReadBookActivity : BaseReadBookActivity(),
} }
} }
private fun skipToSearch(index: Int, indexWithinChapter: Int) { override fun navigateToSearch(searchResult: SearchResult) {
viewModel.openChapter(index) { skipToSearch(searchResult)
val pages = ReadBook.curTextChapter?.pages ?: return@openChapter }
val positions = viewModel.searchResultPositions(pages, indexWithinChapter)
private fun skipToSearch(searchResult: SearchResult) {
val previousResult = binding.searchMenu.previousSearchResult
fun jumpToPosition(){
ReadBook.curTextChapter?.let {
binding.searchMenu.updateSearchInfo()
val positions = viewModel.searchResultPositions(it, searchResult)
ReadBook.skipToPage(positions[0]) { ReadBook.skipToPage(positions[0]) {
launch { launch {
isSelectingSearchResult = true
binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2]) binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2])
delay(20L)
when (positions[3]) { when (positions[3]) {
0 -> binding.readView.curPage.selectEndMoveIndex( 0 -> binding.readView.curPage.selectEndMoveIndex(
0, 0, positions[1], positions[2] + viewModel.searchContentQuery.length - 1
positions[1],
positions[2] + viewModel.searchContentQuery.length - 1
) )
1 -> binding.readView.curPage.selectEndMoveIndex( 1 -> binding.readView.curPage.selectEndMoveIndex(
0, 0, positions[1] + 1, positions[4]
positions[1] + 1,
positions[4]
) )
//consider change page, jump to scroll position //consider change page, jump to scroll position
-1 -> binding.readView.curPage -1 -> binding.readView.curPage.selectEndMoveIndex(1, 0, positions[4])
.selectEndMoveIndex(1, 0, positions[4])
} }
binding.readView.isTextSelected = true binding.readView.isTextSelected = true
delay(100L) isSelectingSearchResult = false
}
} }
} }
} }
if (previousResult.chapterIndex != searchResult.chapterIndex) {
viewModel.openChapter(searchResult.chapterIndex) {
jumpToPosition()
}
} else {
jumpToPosition()
}
} }
private fun startBackupJob() { private fun startBackupJob() {

@ -21,7 +21,8 @@ import io.legado.app.model.ReadBook
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.searchContent.SearchResult
import io.legado.app.utils.msg import io.legado.app.utils.msg
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
@ -270,17 +271,16 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
* 内容搜索跳转 * 内容搜索跳转
*/ */
fun searchResultPositions( fun searchResultPositions(
pages: List<TextPage>, textChapter: TextChapter,
indexWithinChapter: Int searchResult: SearchResult
): Array<Int> { ): Array<Int> {
// calculate search result's pageIndex // calculate search result's pageIndex
var content = "" val pages = textChapter.pages
pages.map { val content = textChapter.getContent()
content += it.text
} var count = 0
var count = 1
var index = content.indexOf(searchContentQuery) var index = content.indexOf(searchContentQuery)
while (count != indexWithinChapter) { while (count != searchResult.resultCountWithinChapter) {
index = content.indexOf(searchContentQuery, index + 1) index = content.indexOf(searchContentQuery, index + 1)
count += 1 count += 1
} }

@ -0,0 +1,235 @@
package io.legado.app.ui.book.read
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import android.view.animation.Animation
import android.widget.FrameLayout
import androidx.core.view.isVisible
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.databinding.ViewSearchMenuBinding
import io.legado.app.help.*
import io.legado.app.lib.theme.*
import io.legado.app.model.ReadBook
import io.legado.app.ui.book.searchContent.SearchResult
import io.legado.app.utils.*
import splitties.views.*
/**
* 搜索界面菜单
*/
class SearchMenu @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
private val callBack: CallBack get() = activity as CallBack
private val binding = ViewSearchMenuBinding.inflate(LayoutInflater.from(context), this, true)
private val menuBottomIn: Animation = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_in)
private val menuBottomOut: Animation = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_out)
private val bgColor: Int = context.bottomBackground
private val textColor: Int = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor))
private val bottomBackgroundList: ColorStateList =
Selector.colorBuild().setDefaultColor(bgColor).setPressedColor(ColorUtils.darkenColor(bgColor)).create()
private var onMenuOutEnd: (() -> Unit)? = null
private val searchResultList: MutableList<SearchResult> = mutableListOf()
private var currentSearchResultIndex: Int = 0
private var lastSearchResultIndex: Int = 0
private val hasSearchResult: Boolean
get() = searchResultList.isNotEmpty()
val selectedSearchResult: SearchResult?
get() = if (searchResultList.isNotEmpty()) searchResultList[currentSearchResultIndex] else null
val previousSearchResult: SearchResult
get() = searchResultList[lastSearchResultIndex]
init {
initAnimation()
initView()
bindEvent()
updateSearchInfo()
observeSearchResultList()
}
private fun observeSearchResultList() {
activity?.let { owner ->
eventObservable<List<SearchResult>>(EventBus.SEARCH_RESULT).observe(owner, {
searchResultList.clear()
searchResultList.addAll(it)
updateSearchInfo()
})
}
}
private fun initView() = binding.run {
llSearchBaseInfo.setBackgroundColor(bgColor)
tvCurrentSearchInfo.setTextColor(bottomBackgroundList)
llBottomBg.setBackgroundColor(bgColor)
fabLeft.backgroundTintList = bottomBackgroundList
fabLeft.setColorFilter(textColor)
fabRight.backgroundTintList = bottomBackgroundList
fabRight.setColorFilter(textColor)
tvMainMenu.setTextColor(textColor)
tvSearchResults.setTextColor(textColor)
tvSearchExit.setTextColor(textColor)
//tvSetting.setTextColor(textColor)
ivMainMenu.setColorFilter(textColor)
ivSearchResults.setColorFilter(textColor)
ivSearchExit.setColorFilter(textColor)
//ivSetting.setColorFilter(textColor)
ivSearchContentUp.setColorFilter(textColor)
ivSearchContentDown.setColorFilter(textColor)
tvCurrentSearchInfo.setTextColor(textColor)
}
fun runMenuIn() {
this.visible()
binding.llSearchBaseInfo.visible()
binding.llBottomBg.visible()
binding.vwMenuBg.visible()
binding.llSearchBaseInfo.startAnimation(menuBottomIn)
binding.llBottomBg.startAnimation(menuBottomIn)
}
fun runMenuOut(onMenuOutEnd: (() -> Unit)? = null) {
this.onMenuOutEnd = onMenuOutEnd
if (this.isVisible) {
binding.llSearchBaseInfo.startAnimation(menuBottomOut)
binding.llBottomBg.startAnimation(menuBottomOut)
}
}
fun updateSearchInfo() {
ReadBook.curTextChapter?.let {
binding.tvCurrentSearchInfo.text = context.getString(R.string.search_content_size) + ": ${searchResultList.size} / 当前章节: ${it.title}"
}
}
fun updateSearchResultIndex(updateIndex: Int) {
lastSearchResultIndex = currentSearchResultIndex
currentSearchResultIndex = when {
updateIndex < 0 -> 0
updateIndex >= searchResultList.size -> searchResultList.size - 1
else -> updateIndex
}
}
private fun bindEvent() = binding.run {
llSearchResults.setOnClickListener {
runMenuOut {
callBack.openSearchActivity(selectedSearchResult?.query)
}
}
//主菜单
llMainMenu.setOnClickListener {
runMenuOut {
callBack.showMenuBar()
this@SearchMenu.invisible()
}
}
//目录
llSearchExit.setOnClickListener {
runMenuOut {
callBack.exitSearchMenu()
this@SearchMenu.invisible()
}
}
//设置
// llSetting.setOnClickListener {
// runMenuOut {
// callBack.showSearchSetting()
// }
// }
fabLeft.setOnClickListener {
updateSearchResultIndex(currentSearchResultIndex - 1)
callBack.navigateToSearch(searchResultList[currentSearchResultIndex])
}
ivSearchContentUp.setOnClickListener {
updateSearchResultIndex(currentSearchResultIndex - 1)
callBack.navigateToSearch(searchResultList[currentSearchResultIndex])
}
ivSearchContentDown.setOnClickListener {
updateSearchResultIndex(currentSearchResultIndex + 1)
callBack.navigateToSearch(searchResultList[currentSearchResultIndex])
}
fabRight.setOnClickListener {
updateSearchResultIndex(currentSearchResultIndex + 1)
callBack.navigateToSearch(searchResultList[currentSearchResultIndex])
}
}
private fun initAnimation() {
//显示菜单
menuBottomIn.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
callBack.upSystemUiVisibility()
binding.fabLeft.visible(hasSearchResult)
binding.fabRight.visible(hasSearchResult)
}
@SuppressLint("RtlHardcoded")
override fun onAnimationEnd(animation: Animation) {
val navigationBarHeight = if (ReadBookConfig.hideNavigationBar) {
activity?.navigationBarHeight ?: 0
} else {
0
}
binding.run {
vwMenuBg.setOnClickListener { runMenuOut() }
root.padding = 0
when (activity?.navigationBarGravity) {
Gravity.BOTTOM -> root.bottomPadding = navigationBarHeight
Gravity.LEFT -> root.leftPadding = navigationBarHeight
Gravity.RIGHT -> root.rightPadding = navigationBarHeight
}
}
callBack.upSystemUiVisibility()
}
override fun onAnimationRepeat(animation: Animation) = Unit
})
//隐藏菜单
menuBottomOut.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
binding.vwMenuBg.setOnClickListener(null)
}
override fun onAnimationEnd(animation: Animation) {
binding.llSearchBaseInfo.invisible()
binding.llBottomBg.invisible()
binding.vwMenuBg.invisible()
binding.vwMenuBg.setOnClickListener { runMenuOut() }
onMenuOutEnd?.invoke()
callBack.upSystemUiVisibility()
}
override fun onAnimationRepeat(animation: Animation) = Unit
})
}
interface CallBack {
var isShowingSearchResult: Boolean
fun openSearchActivity(searchWord: String?)
fun showSearchSetting()
fun upSystemUiVisibility()
fun exitSearchMenu()
fun showMenuBar()
fun navigateToSearch(searchResult: SearchResult)
}
}

@ -145,7 +145,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (it.isImage) { if (it.isImage) {
drawImage(canvas, it, lineTop, lineBottom, isImageLine) drawImage(canvas, it, lineTop, lineBottom, isImageLine)
} else { } else {
if(it.isSearchResult) {
textPaint.color = context.accentColor
}
canvas.drawText(it.charData, it.start, lineBase, textPaint) canvas.drawText(it.charData, it.start, lineBase, textPaint)
textPaint.color = ReadBookConfig.textColor
} }
if (it.selected) { if (it.selected) {
canvas.drawRect(it.start, lineTop, it.end, lineBottom, selectedPaint) canvas.drawRect(it.start, lineTop, it.end, lineBottom, selectedPaint)
@ -392,6 +396,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
relativePos in selectStart[0] + 1 until selectEnd[0] relativePos in selectStart[0] + 1 until selectEnd[0]
} }
} }
textChar.isSearchResult = textChar.selected && callBack.isSelectingSearchResult
} }
} }
} }
@ -539,5 +544,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
val headerHeight: Int val headerHeight: Int
val pageFactory: TextPageFactory val pageFactory: TextPageFactory
val isScroll: Boolean val isScroll: Boolean
var isSelectingSearchResult: Boolean
} }
} }

@ -5,7 +5,8 @@ data class TextChar(
var start: Float, var start: Float,
var end: Float, var end: Float,
var selected: Boolean = false, var selected: Boolean = false,
var isImage: Boolean = false var isImage: Boolean = false,
var isSearchResult: Boolean = false
) { ) {
fun isTouch(x: Float): Boolean { fun isTouch(x: Float): Boolean {

@ -5,7 +5,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import com.github.liuyueyi.quick.transfer.ChineseUtils
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
@ -13,7 +12,6 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.databinding.ActivitySearchContentBinding import io.legado.app.databinding.ActivitySearchContentBinding
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.getPrimaryTextColor
@ -23,6 +21,7 @@ import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.ColorUtils import io.legado.app.utils.ColorUtils
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.observeEvent import io.legado.app.utils.observeEvent
import io.legado.app.utils.postEvent
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -40,9 +39,7 @@ class SearchContentActivity :
private val searchView: SearchView by lazy { private val searchView: SearchView by lazy {
binding.titleBar.findViewById(R.id.search_view) binding.titleBar.findViewById(R.id.search_view)
} }
private var searchResultCounts = 0
private var durChapterIndex = 0 private var durChapterIndex = 0
private var searchResultList: MutableList<SearchResult> = mutableListOf()
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
val bbg = bottomBackground val bbg = bottomBackground
@ -103,7 +100,7 @@ class SearchContentActivity :
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun initBook() { private fun initBook() {
binding.tvCurrentSearchInfo.text = "搜索结果:$searchResultCounts" binding.tvCurrentSearchInfo.text = this.getString(R.string.search_content_size) +": ${viewModel.searchResultCounts}"
viewModel.book?.let { viewModel.book?.let {
initCacheFileNames(it) initCacheFileNames(it)
durChapterIndex = it.durChapterIndex durChapterIndex = it.durChapterIndex
@ -115,7 +112,7 @@ class SearchContentActivity :
private fun initCacheFileNames(book: Book) { private fun initCacheFileNames(book: Book) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book)) viewModel.cacheChapterNames.addAll(BookHelp.getChapterFiles(book))
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
adapter.notifyItemRangeChanged(0, adapter.itemCount, true) adapter.notifyItemRangeChanged(0, adapter.itemCount, true)
} }
@ -126,7 +123,7 @@ class SearchContentActivity :
observeEvent<BookChapter>(EventBus.SAVE_CONTENT) { chapter -> observeEvent<BookChapter>(EventBus.SAVE_CONTENT) { chapter ->
viewModel.book?.bookUrl?.let { bookUrl -> viewModel.book?.bookUrl?.let { bookUrl ->
if (chapter.bookUrl == bookUrl) { if (chapter.bookUrl == bookUrl) {
adapter.cacheFileNames.add(chapter.getFileName()) viewModel.cacheChapterNames.add(chapter.getFileName())
adapter.notifyItemChanged(chapter.index, true) adapter.notifyItemChanged(chapter.index, true)
} }
} }
@ -134,28 +131,26 @@ class SearchContentActivity :
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun startContentSearch(newText: String) { fun startContentSearch(query: String) {
// 按章节搜索内容 // 按章节搜索内容
if (newText.isNotBlank()) { if (query.isNotBlank()) {
adapter.clearItems() adapter.clearItems()
searchResultList.clear() viewModel.searchResultList.clear()
binding.refreshProgressBar.isAutoLoading = true viewModel.searchResultCounts = 0
searchResultCounts = 0 viewModel.lastQuery = query
viewModel.lastQuery = newText
var searchResults = listOf<SearchResult>() var searchResults = listOf<SearchResult>()
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
appDb.bookChapterDao.getChapterList(viewModel.bookUrl).map { chapter -> appDb.bookChapterDao.getChapterList(viewModel.bookUrl).map { bookChapter ->
binding.refreshProgressBar.isAutoLoading = true
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (isLocalBook if (isLocalBook || viewModel.cacheChapterNames.contains(bookChapter.getFileName())) {
|| adapter.cacheFileNames.contains(chapter.getFileName()) searchResults = viewModel.searchChapter(query, bookChapter)
) {
searchResults = searchChapter(newText, chapter)
} }
} }
if (searchResults.isNotEmpty()) { if (searchResults.isNotEmpty()) {
searchResultList.addAll(searchResults) viewModel.searchResultList.addAll(searchResults)
binding.refreshProgressBar.isAutoLoading = false binding.refreshProgressBar.isAutoLoading = false
binding.tvCurrentSearchInfo.text = "搜索结果:$searchResultCounts" binding.tvCurrentSearchInfo.text = this@SearchContentActivity.getString(R.string.search_content_size) +": ${viewModel.searchResultCounts}"
adapter.addItems(searchResults) adapter.addItems(searchResults)
searchResults = listOf() searchResults = listOf()
} }
@ -164,94 +159,17 @@ class SearchContentActivity :
} }
} }
private suspend fun searchChapter(query: String, chapter: BookChapter?): List<SearchResult> {
val searchResults: MutableList<SearchResult> = mutableListOf()
var positions: List<Int>
var replaceContents: List<String>?
var totalContents: String
if (chapter != null) {
viewModel.book?.let { book ->
val bookContent = BookHelp.getContent(book, chapter)
if (bookContent != null) {
//搜索替换后的正文
withContext(Dispatchers.IO) {
chapter.title = when (AppConfig.chineseConverterType) {
1 -> ChineseUtils.t2s(chapter.title)
2 -> ChineseUtils.s2t(chapter.title)
else -> chapter.title
}
replaceContents =
viewModel.contentProcessor!!.getContent(
book,
chapter,
bookContent,
chineseConvert = false,
reSegment = false
)
}
totalContents = replaceContents?.joinToString("") ?: bookContent
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 val isLocalBook: Boolean
get() = viewModel.book?.isLocalBook() == true get() = viewModel.book?.isLocalBook() == true
override fun openSearchResult(searchResult: SearchResult) { override fun openSearchResult(searchResult: SearchResult) {
postEvent(EventBus.SEARCH_RESULT, viewModel.searchResultList as List<SearchResult>)
val searchData = Intent() val searchData = Intent()
searchData.putExtra("index", searchResult.chapterIndex) searchData.putExtra("searchResultIndex", viewModel.searchResultList.indexOf(searchResult))
searchData.putExtra("contentPosition", searchResult.contentPosition) searchData.putExtra("chapterIndex", searchResult.chapterIndex)
searchData.putExtra("contentPosition", searchResult.queryIndexInChapter)
searchData.putExtra("query", searchResult.query) searchData.putExtra("query", searchResult.query)
searchData.putExtra("indexWithinChapter", searchResult.indexWithinChapter) searchData.putExtra("resultCountWithinChapter", searchResult.resultCountWithinChapter)
setResult(RESULT_OK, searchData) setResult(RESULT_OK, searchData)
finish() finish()
} }

@ -2,16 +2,26 @@ package io.legado.app.ui.book.searchContent
import android.app.Application import android.app.Application
import com.github.liuyueyi.quick.transfer.ChineseUtils
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book 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.help.ContentProcessor import io.legado.app.help.ContentProcessor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class SearchContentViewModel(application: Application) : BaseViewModel(application) { class SearchContentViewModel(application: Application) : BaseViewModel(application) {
var bookUrl: String = "" var bookUrl: String = ""
var book: Book? = null var book: Book? = null
var contentProcessor: ContentProcessor? = null private var contentProcessor: ContentProcessor? = null
var lastQuery: String = "" var lastQuery: String = ""
var searchResultCounts = 0
val cacheChapterNames = hashSetOf<String>()
val searchResultList: MutableList<SearchResult> = mutableListOf()
var selectedIndex = 0
fun initBook(bookUrl: String, success: () -> Unit) { fun initBook(bookUrl: String, success: () -> Unit) {
this.bookUrl = bookUrl this.bookUrl = bookUrl
@ -25,4 +35,72 @@ class SearchContentViewModel(application: Application) : BaseViewModel(applicati
} }
} }
suspend fun searchChapter(query: String, chapter: BookChapter?): List<SearchResult> {
val searchResultsWithinChapter: MutableList<SearchResult> = mutableListOf()
if (chapter != null) {
book?.let { book ->
val chapterContent = BookHelp.getContent(book, chapter)
if (chapterContent != null) {
//搜索替换后的正文
val replaceContent: String
withContext(Dispatchers.IO) {
chapter.title = when (AppConfig.chineseConverterType) {
1 -> ChineseUtils.t2s(chapter.title)
2 -> ChineseUtils.s2t(chapter.title)
else -> chapter.title
}
replaceContent = contentProcessor!!.getContent(
book, chapter, chapterContent, chineseConvert = false, reSegment = false
).joinToString("")
}
val positions = searchPosition(replaceContent, query)
positions.forEachIndexed { index, position ->
val construct = getResultAndQueryIndex(replaceContent, position, query)
val result = SearchResult(
resultCountWithinChapter = index,
resultText = construct.second,
chapterTitle = chapter.title,
query = query,
chapterIndex = chapter.index,
queryIndexInResult = construct.first,
queryIndexInChapter = position
)
searchResultsWithinChapter.add(result)
}
searchResultCounts += searchResultsWithinChapter.size
}
}
}
return searchResultsWithinChapter
}
private fun searchPosition(chapterContent: String, pattern: String): List<Int> {
val position: MutableList<Int> = mutableListOf()
var index = chapterContent.indexOf(pattern)
while (index >= 0) {
position.add(index)
index = chapterContent.indexOf(pattern, index + 1)
}
return position
}
private fun getResultAndQueryIndex(content: String, queryIndexInContent: Int, query: String): Pair<Int, String> {
// 左右移动20个字符,构建关键词周边文字,在搜索结果里显示
// todo: 判断段落,只在关键词所在段落内分割
// todo: 利用标点符号分割完整的句
// todo: length和设置结合,自由调整周边文字长度
val length = 20
var po1 = queryIndexInContent - length
var po2 = queryIndexInContent + query.length + length
if (po1 < 0) {
po1 = 0
}
if (po2 > content.length) {
po2 = content.length
}
val queryIndexInResult = queryIndexInContent - po1
val newText = content.substring(po1, po2)
return queryIndexInResult to newText
}
} }

@ -4,36 +4,29 @@ import android.text.Spanned
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
data class SearchResult( data class SearchResult(
var index: Int = 0, val resultCount: Int = 0,
var indexWithinChapter: Int = 0, val resultCountWithinChapter: Int = 0,
var text: String = "", val resultText: String = "",
var chapterTitle: String = "", val chapterTitle: String = "",
val query: String, val query: String,
var pageSize: Int = 0, val pageSize: Int = 0,
var chapterIndex: Int = 0, val chapterIndex: Int = 0,
var pageIndex: Int = 0, val pageIndex: Int = 0,
var newPosition: Int = 0, val queryIndexInResult: Int = 0,
var contentPosition: Int = 0 val queryIndexInChapter: Int = 0
) { ) {
fun getHtmlCompat(textColor: String, accentColor: String): Spanned { fun getHtmlCompat(textColor: String, accentColor: String): Spanned {
val html = colorPresentText(newPosition, query, text, textColor, accentColor) + val queryIndexInSurrounding = resultText.indexOf(query)
"<font color=#${accentColor}>($chapterTitle)</font>" val leftString = resultText.substring(0, queryIndexInSurrounding)
val rightString = resultText.substring(queryIndexInSurrounding + query.length, resultText.length)
val html = leftString.colorTextForHtml(textColor) +
query.colorTextForHtml(accentColor) +
rightString.colorTextForHtml(textColor) +
chapterTitle.colorTextForHtml(accentColor)
return HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY) return HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY)
} }
private fun colorPresentText( private fun String.colorTextForHtml(textColor: String) = "<font color=#${textColor}>$this</font>"
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)
return "<font color=#${textColor}>$sub1</font>" +
"<font color=#${accentColor}>$center</font>" +
"<font color=#${textColor}>$sub2</font>"
}
} }

@ -37,6 +37,12 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" /> android:visibility="gone" />
<io.legado.app.ui.book.read.SearchMenu
android:id="@+id/search_menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<View <View
android:id="@+id/navigation_bar" android:id="@+id/navigation_bar"
android:layout_width="match_parent" android:layout_width="match_parent"

@ -0,0 +1,276 @@
<?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:orientation="vertical">
<View
android:id="@+id/vw_menu_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/content"
tools:layout_editor_absoluteX="0dp"
tools:layout_editor_absoluteY="0dp"
tools:visibility="invisible"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabLeft"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="上个结果"
android:rotation="180"
android:src="@drawable/ic_arrow_right"
android:tint="@color/primaryText"
android:tooltipText="@string/search_content"
app:backgroundTint="@color/background_menu"
app:elevation="2dp"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:pressedTranslationZ="2dp"
tools:ignore="UnusedAttribute" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabRight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="下个结果"
android:src="@drawable/ic_arrow_right"
android:tint="@color/primaryText"
android:tooltipText="@string/search_content"
app:backgroundTint="@color/background_menu"
app:elevation="2dp"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:pressedTranslationZ="2dp"
tools:ignore="UnusedAttribute" />
<LinearLayout
android:id="@+id/ll_search_base_info"
android:layout_width="match_parent"
android:layout_height="36dp"
android:background="@color/background_menu"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="10dp"
android:paddingRight="10dp"
app:layout_constraintBottom_toTopOf="@id/ll_bottom_bg">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_search_content_up"
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_down"
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" />
<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" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_bottom_bg"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="8dp"
android:baselineAligned="false"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent">
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:importantForAccessibility="no" />
<!--结果按钮-->
<LinearLayout
android:id="@+id/ll_search_results"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="结果"
android:focusable="true"
android:orientation="vertical"
android:paddingBottom="7dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_search_results"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:contentDescription="结果"
android:src="@drawable/ic_toc"
app:tint="@color/primaryText"
tools:ignore="NestedWeights" />
<TextView
android:id="@+id/tv_search_results"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="3dp"
android:maxLines="1"
android:text="结果"
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_main_menu"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/read_aloud"
android:focusable="true"
android:orientation="vertical"
android:paddingBottom="7dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_main_menu"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:contentDescription="@string/main_menu"
android:src="@drawable/ic_menu"
app:tint="@color/primaryText"
tools:ignore="NestedWeights" />
<TextView
android:id="@+id/tv_main_menu"
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/main_menu"
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_search_exit"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="退出"
android:focusable="true"
android:orientation="vertical"
android:paddingBottom="7dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_search_exit"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:contentDescription="退出"
android:src="@drawable/ic_auto_page_stop"
app:tint="@color/primaryText"
tools:ignore="NestedWeights" />
<TextView
android:id="@+id/tv_search_exit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="3dp"
android:maxLines="1"
android:text="退出"
android:textColor="@color/primaryText"
android:textSize="12sp" />
</LinearLayout>
<!-- <View-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="match_parent"-->
<!-- android:layout_weight="2" />-->
<!-- &lt;!&ndash;设置按钮&ndash;&gt;-->
<!-- <LinearLayout-->
<!-- android:id="@+id/ll_setting"-->
<!-- android:layout_width="50dp"-->
<!-- android:layout_height="50dp"-->
<!-- android:background="?android:attr/selectableItemBackgroundBorderless"-->
<!-- android:clickable="true"-->
<!-- android:contentDescription="@string/setting"-->
<!-- android:focusable="true"-->
<!-- android:orientation="vertical"-->
<!-- android:paddingBottom="7dp">-->
<!-- <androidx.appcompat.widget.AppCompatImageView-->
<!-- android:id="@+id/iv_setting"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="0dp"-->
<!-- android:layout_weight="1"-->
<!-- android:contentDescription="@string/aloud_config"-->
<!-- android:src="@drawable/ic_settings"-->
<!-- app:tint="@color/primaryText"-->
<!-- tools:ignore="NestedWeights" />-->
<!-- <TextView-->
<!-- android:id="@+id/tv_setting"-->
<!-- 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/setting"-->
<!-- android:textColor="@color/primaryText"-->
<!-- android:textSize="12sp" />-->
<!-- </LinearLayout>-->
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:importantForAccessibility="no" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -902,5 +902,6 @@
<string name="login_header">登录头</string> <string name="login_header">登录头</string>
<string name="font_scale">字体大小</string> <string name="font_scale">字体大小</string>
<string name="font_scale_summary">当前字体大小:%.1f</string> <string name="font_scale_summary">当前字体大小:%.1f</string>
<string name="search_content_size">search result</string>
</resources> </resources>

@ -906,5 +906,6 @@
<string name="login_header">登录头</string> <string name="login_header">登录头</string>
<string name="font_scale">字体大小</string> <string name="font_scale">字体大小</string>
<string name="font_scale_summary">当前字体大小:%.1f</string> <string name="font_scale_summary">当前字体大小:%.1f</string>
<string name="search_content_size">search result</string>
</resources> </resources>

@ -906,5 +906,6 @@
<string name="login_header">登录头</string> <string name="login_header">登录头</string>
<string name="font_scale">字体大小</string> <string name="font_scale">字体大小</string>
<string name="font_scale_summary">当前字体大小:%.1f</string> <string name="font_scale_summary">当前字体大小:%.1f</string>
<string name="search_content_size">search result</string>
</resources> </resources>

@ -903,5 +903,6 @@
<string name="login_header">登录头</string> <string name="login_header">登录头</string>
<string name="font_scale">字体大小</string> <string name="font_scale">字体大小</string>
<string name="font_scale_summary">当前字体大小:%.1f</string> <string name="font_scale_summary">当前字体大小:%.1f</string>
<string name="search_content_size">搜索結果</string>
</resources> </resources>

@ -905,5 +905,6 @@
<string name="login_header">登录头</string> <string name="login_header">登录头</string>
<string name="font_scale">字体大小</string> <string name="font_scale">字体大小</string>
<string name="font_scale_summary">当前字体大小:%.1f</string> <string name="font_scale_summary">当前字体大小:%.1f</string>
<string name="search_content_size">搜索結果</string>
</resources> </resources>

@ -776,6 +776,7 @@
<string name="select_theme">切换默认主题</string> <string name="select_theme">切换默认主题</string>
<string name="sort_by_lastUpdateTime">更新时间排序</string> <string name="sort_by_lastUpdateTime">更新时间排序</string>
<string name="search_content">全文搜索</string> <string name="search_content">全文搜索</string>
<string name="search_content_size">搜索结果</string>
<string name="rss_source_empty">关注公众号[开源阅读]获取订阅源!</string> <string name="rss_source_empty">关注公众号[开源阅读]获取订阅源!</string>
<string name="explore_empty">当前没有发现源,关注公众号[开源阅读]添加带发现的书源!</string> <string name="explore_empty">当前没有发现源,关注公众号[开源阅读]添加带发现的书源!</string>
<string name="page_key_set_help">将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开.</string> <string name="page_key_set_help">将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开.</string>

@ -906,5 +906,6 @@
<string name="login_header">login header</string> <string name="login_header">login header</string>
<string name="font_scale">font scale</string> <string name="font_scale">font scale</string>
<string name="font_scale_summary">font scale:%.1f</string> <string name="font_scale_summary">font scale:%.1f</string>
<string name="search_content_size">search result</string>
</resources> </resources>

Loading…
Cancel
Save