From adaae819d91a88d0daec984a92273facbeed6681 Mon Sep 17 00:00:00 2001 From: Jason Yao Date: Tue, 14 Dec 2021 23:17:16 -0500 Subject: [PATCH] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E8=8F=9C=E5=8D=95=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 左右跳转搜索结果 已实现,跳转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 --- .../java/io/legado/app/constant/EventBus.kt | 1 + .../app/ui/book/read/ReadBookActivity.kt | 98 +++++-- .../app/ui/book/read/ReadBookViewModel.kt | 18 +- .../io/legado/app/ui/book/read/SearchMenu.kt | 235 +++++++++++++++ .../app/ui/book/read/page/ContentTextView.kt | 6 + .../ui/book/read/page/entities/TextChar.kt | 3 +- .../searchContent/SearchContentActivity.kt | 122 ++------ .../searchContent/SearchContentViewModel.kt | 80 ++++- .../app/ui/book/searchContent/SearchResult.kt | 41 ++- .../main/res/layout/activity_book_read.xml | 6 + app/src/main/res/layout/view_search_menu.xml | 276 ++++++++++++++++++ app/src/main/res/values-es-rES/strings.xml | 1 + app/src/main/res/values-ja-rJP/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-zh-rHK/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values-zh/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 18 files changed, 724 insertions(+), 169 deletions(-) create mode 100644 app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt create mode 100644 app/src/main/res/layout/view_search_menu.xml diff --git a/app/src/main/java/io/legado/app/constant/EventBus.kt b/app/src/main/java/io/legado/app/constant/EventBus.kt index 7b53c4cbd..37bbf4be9 100644 --- a/app/src/main/java/io/legado/app/constant/EventBus.kt +++ b/app/src/main/java/io/legado/app/constant/EventBus.kt @@ -27,4 +27,5 @@ object EventBus { const val CHECK_SOURCE_DONE = "checkSourceDone" const val TIP_COLOR = "tipColor" const val SOURCE_CHANGED = "sourceChanged" + const val SEARCH_RESULT = "searchResult" } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 755ddade9..a07fee48f 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -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.provider.TextPageFactory 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.toc.BookmarkDialog import io.legado.app.ui.book.toc.TocActivityResult @@ -65,6 +66,7 @@ class ReadBookActivity : BaseReadBookActivity(), TextActionMenu.CallBack, ContentTextView.CallBack, ReadMenu.CallBack, + SearchMenu.CallBack, ReadAloudDialog.CallBack, ChangeSourceDialog.CallBack, ReadBook.CallBack, @@ -93,17 +95,21 @@ class ReadBookActivity : BaseReadBookActivity(), viewModel.replaceRuleChanged() } } - private val searchContentActivity = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - it ?: return@registerForActivityResult - it.data?.let { data -> - data.getIntExtra("index", ReadBook.durChapterIndex).let { index -> - viewModel.searchContentQuery = data.getStringExtra("query") ?: "" - val indexWithinChapter = data.getIntExtra("indexWithinChapter", 0) - skipToSearch(index, indexWithinChapter) + private val searchContentActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + it ?: return@registerForActivityResult + it.data?.let { data -> + data.getIntExtra("chapterIndex", ReadBook.durChapterIndex).let { _ -> + viewModel.searchContentQuery = data.getStringExtra("query") ?: "" + val searchResultIndex = data.getIntExtra("searchResultIndex", 0) + isShowingSearchResult = true + binding.searchMenu.updateSearchResultIndex(searchResultIndex) + binding.searchMenu.selectedSearchResult?.let { currentResult -> + skipToSearch(currentResult) + showActionMenu() } } } + } private var menu: Menu? = null val textActionMenu: TextActionMenu by lazy { TextActionMenu(this, this) @@ -116,6 +122,11 @@ class ReadBookActivity : BaseReadBookActivity(), private var backupJob: Job? = null override var autoPageProgress = 0 override var isAutoPage = false + override var isShowingSearchResult = false + override var isSelectingSearchResult = false + set(value) { + field = value && isShowingSearchResult + } private var screenTimeOut: Long = 0 private var timeBatteryReceiver: TimeBatteryReceiver? = null private var loadStates: Boolean = false @@ -634,6 +645,7 @@ class ReadBookActivity : BaseReadBookActivity(), when { BaseReadAloudService.isRun -> showReadAloudDialog() isAutoPage -> showDialogFragment() + isShowingSearchResult -> binding.searchMenu.runMenuIn() else -> binding.readMenu.runMenuIn() } } @@ -765,6 +777,10 @@ class ReadBookActivity : BaseReadBookActivity(), showDialogFragment() } + override fun showSearchSetting() { + showDialogFragment() + } + /** * 更新状态栏,导航栏 */ @@ -773,6 +789,13 @@ class ReadBookActivity : BaseReadBookActivity(), upNavigationBarColor() } + override fun exitSearchMenu() { + if (isShowingSearchResult) { + isShowingSearchResult = false + binding.searchMenu.invalidate() + } + } + override fun showLogin() { ReadBook.bookSource?.let { startActivity { @@ -869,34 +892,45 @@ class ReadBookActivity : BaseReadBookActivity(), } } - private fun skipToSearch(index: Int, indexWithinChapter: Int) { - viewModel.openChapter(index) { - val pages = ReadBook.curTextChapter?.pages ?: return@openChapter - val positions = viewModel.searchResultPositions(pages, indexWithinChapter) - ReadBook.skipToPage(positions[0]) { - launch { - binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2]) - delay(20L) - when (positions[3]) { - 0 -> binding.readView.curPage.selectEndMoveIndex( - 0, - positions[1], - positions[2] + viewModel.searchContentQuery.length - 1 - ) - 1 -> binding.readView.curPage.selectEndMoveIndex( - 0, - positions[1] + 1, - positions[4] - ) - //consider change page, jump to scroll position - -1 -> binding.readView.curPage - .selectEndMoveIndex(1, 0, positions[4]) + override fun navigateToSearch(searchResult: SearchResult) { + skipToSearch(searchResult) + } + + 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]) { + launch { + isSelectingSearchResult = true + binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2]) + when (positions[3]) { + 0 -> binding.readView.curPage.selectEndMoveIndex( + 0, positions[1], positions[2] + viewModel.searchContentQuery.length - 1 + ) + 1 -> binding.readView.curPage.selectEndMoveIndex( + 0, positions[1] + 1, positions[4] + ) + //consider change page, jump to scroll position + -1 -> binding.readView.curPage.selectEndMoveIndex(1, 0, positions[4]) + } + binding.readView.isTextSelected = true + isSelectingSearchResult = false } - binding.readView.isTextSelected = true - delay(100L) } } } + + if (previousResult.chapterIndex != searchResult.chapterIndex) { + viewModel.openChapter(searchResult.chapterIndex) { + jumpToPosition() + } + } else { + jumpToPosition() + } } private fun startBackupJob() { diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index d5f016c18..eec3f32f6 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -21,7 +21,8 @@ import io.legado.app.model.ReadBook import io.legado.app.model.localBook.LocalBook import io.legado.app.model.webBook.WebBook 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.postEvent import io.legado.app.utils.toastOnUi @@ -270,17 +271,16 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { * 内容搜索跳转 */ fun searchResultPositions( - pages: List, - indexWithinChapter: Int + textChapter: TextChapter, + searchResult: SearchResult ): Array { // calculate search result's pageIndex - var content = "" - pages.map { - content += it.text - } - var count = 1 + val pages = textChapter.pages + val content = textChapter.getContent() + + var count = 0 var index = content.indexOf(searchContentQuery) - while (count != indexWithinChapter) { + while (count != searchResult.resultCountWithinChapter) { index = content.indexOf(searchContentQuery, index + 1) count += 1 } diff --git a/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt new file mode 100644 index 000000000..6e176c437 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt @@ -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 = 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>(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) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt index 91e182c47..08fa3ca26 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt @@ -145,7 +145,11 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at if (it.isImage) { drawImage(canvas, it, lineTop, lineBottom, isImageLine) } else { + if(it.isSearchResult) { + textPaint.color = context.accentColor + } canvas.drawText(it.charData, it.start, lineBase, textPaint) + textPaint.color = ReadBookConfig.textColor } if (it.selected) { 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] } } + textChar.isSearchResult = textChar.selected && callBack.isSelectingSearchResult } } } @@ -539,5 +544,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at val headerHeight: Int val pageFactory: TextPageFactory val isScroll: Boolean + var isSelectingSearchResult: Boolean } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt index abe2e0813..518067182 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt @@ -5,7 +5,8 @@ data class TextChar( var start: Float, var end: Float, var selected: Boolean = false, - var isImage: Boolean = false + var isImage: Boolean = false, + var isSearchResult: Boolean = false ) { fun isTouch(x: Float): Boolean { diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt index 5bb46882e..274cc4fd3 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt @@ -5,7 +5,6 @@ import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.widget.SearchView -import com.github.liuyueyi.quick.transfer.ChineseUtils import io.legado.app.R import io.legado.app.base.VMBaseActivity 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.BookChapter import io.legado.app.databinding.ActivitySearchContentBinding -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 @@ -23,6 +21,7 @@ import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.ColorUtils import io.legado.app.utils.applyTint import io.legado.app.utils.observeEvent +import io.legado.app.utils.postEvent import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -40,9 +39,7 @@ class SearchContentActivity : private val searchView: SearchView by lazy { binding.titleBar.findViewById(R.id.search_view) } - private var searchResultCounts = 0 private var durChapterIndex = 0 - private var searchResultList: MutableList = mutableListOf() override fun onActivityCreated(savedInstanceState: Bundle?) { val bbg = bottomBackground @@ -103,7 +100,7 @@ class SearchContentActivity : @SuppressLint("SetTextI18n") private fun initBook() { - binding.tvCurrentSearchInfo.text = "搜索结果:$searchResultCounts" + binding.tvCurrentSearchInfo.text = this.getString(R.string.search_content_size) +": ${viewModel.searchResultCounts}" viewModel.book?.let { initCacheFileNames(it) durChapterIndex = it.durChapterIndex @@ -115,7 +112,7 @@ class SearchContentActivity : private fun initCacheFileNames(book: Book) { launch(Dispatchers.IO) { - adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book)) + viewModel.cacheChapterNames.addAll(BookHelp.getChapterFiles(book)) withContext(Dispatchers.Main) { adapter.notifyItemRangeChanged(0, adapter.itemCount, true) } @@ -126,7 +123,7 @@ class SearchContentActivity : observeEvent(EventBus.SAVE_CONTENT) { chapter -> viewModel.book?.bookUrl?.let { bookUrl -> if (chapter.bookUrl == bookUrl) { - adapter.cacheFileNames.add(chapter.getFileName()) + viewModel.cacheChapterNames.add(chapter.getFileName()) adapter.notifyItemChanged(chapter.index, true) } } @@ -134,28 +131,26 @@ class SearchContentActivity : } @SuppressLint("SetTextI18n") - fun startContentSearch(newText: String) { + fun startContentSearch(query: String) { // 按章节搜索内容 - if (newText.isNotBlank()) { + if (query.isNotBlank()) { adapter.clearItems() - searchResultList.clear() - binding.refreshProgressBar.isAutoLoading = true - searchResultCounts = 0 - viewModel.lastQuery = newText + viewModel.searchResultList.clear() + viewModel.searchResultCounts = 0 + viewModel.lastQuery = query var searchResults = listOf() launch(Dispatchers.Main) { - appDb.bookChapterDao.getChapterList(viewModel.bookUrl).map { chapter -> + appDb.bookChapterDao.getChapterList(viewModel.bookUrl).map { bookChapter -> + binding.refreshProgressBar.isAutoLoading = true withContext(Dispatchers.IO) { - if (isLocalBook - || adapter.cacheFileNames.contains(chapter.getFileName()) - ) { - searchResults = searchChapter(newText, chapter) + if (isLocalBook || viewModel.cacheChapterNames.contains(bookChapter.getFileName())) { + searchResults = viewModel.searchChapter(query, bookChapter) } } if (searchResults.isNotEmpty()) { - searchResultList.addAll(searchResults) + viewModel.searchResultList.addAll(searchResults) binding.refreshProgressBar.isAutoLoading = false - binding.tvCurrentSearchInfo.text = "搜索结果:$searchResultCounts" + binding.tvCurrentSearchInfo.text = this@SearchContentActivity.getString(R.string.search_content_size) +": ${viewModel.searchResultCounts}" adapter.addItems(searchResults) searchResults = listOf() } @@ -164,94 +159,17 @@ class SearchContentActivity : } } - private suspend fun searchChapter(query: String, chapter: BookChapter?): List { - val searchResults: MutableList = mutableListOf() - var positions: List - var replaceContents: List? - 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 { - val position: MutableList = 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 { - // 构建关键词周边文字,在搜索结果里显示 - // 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) { + postEvent(EventBus.SEARCH_RESULT, viewModel.searchResultList as List) val searchData = Intent() - searchData.putExtra("index", searchResult.chapterIndex) - searchData.putExtra("contentPosition", searchResult.contentPosition) + searchData.putExtra("searchResultIndex", viewModel.searchResultList.indexOf(searchResult)) + searchData.putExtra("chapterIndex", searchResult.chapterIndex) + searchData.putExtra("contentPosition", searchResult.queryIndexInChapter) searchData.putExtra("query", searchResult.query) - searchData.putExtra("indexWithinChapter", searchResult.indexWithinChapter) + searchData.putExtra("resultCountWithinChapter", searchResult.resultCountWithinChapter) setResult(RESULT_OK, searchData) finish() } diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt index 121248738..6489e2852 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt @@ -2,16 +2,26 @@ package io.legado.app.ui.book.searchContent import android.app.Application +import com.github.liuyueyi.quick.transfer.ChineseUtils import io.legado.app.base.BaseViewModel import io.legado.app.data.appDb 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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class SearchContentViewModel(application: Application) : BaseViewModel(application) { var bookUrl: String = "" var book: Book? = null - var contentProcessor: ContentProcessor? = null + private var contentProcessor: ContentProcessor? = null var lastQuery: String = "" + var searchResultCounts = 0 + val cacheChapterNames = hashSetOf() + val searchResultList: MutableList = mutableListOf() + var selectedIndex = 0 fun initBook(bookUrl: String, success: () -> Unit) { this.bookUrl = bookUrl @@ -25,4 +35,72 @@ class SearchContentViewModel(application: Application) : BaseViewModel(applicati } } + suspend fun searchChapter(query: String, chapter: BookChapter?): List { + val searchResultsWithinChapter: MutableList = 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 { + val position: MutableList = 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 { + // 左右移动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 + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt index 3dc4e9fc3..a3b3631be 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt @@ -4,36 +4,29 @@ import android.text.Spanned import androidx.core.text.HtmlCompat data class SearchResult( - var index: Int = 0, - var indexWithinChapter: Int = 0, - var text: String = "", - var chapterTitle: String = "", + val resultCount: Int = 0, + val resultCountWithinChapter: Int = 0, + val resultText: String = "", + val chapterTitle: String = "", val query: String, - var pageSize: Int = 0, - var chapterIndex: Int = 0, - var pageIndex: Int = 0, - var newPosition: Int = 0, - var contentPosition: Int = 0 + val pageSize: Int = 0, + val chapterIndex: Int = 0, + val pageIndex: Int = 0, + val queryIndexInResult: Int = 0, + val queryIndexInChapter: Int = 0 ) { fun getHtmlCompat(textColor: String, accentColor: String): Spanned { - val html = colorPresentText(newPosition, query, text, textColor, accentColor) + - "($chapterTitle)" + val queryIndexInSurrounding = resultText.indexOf(query) + 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) } - 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) - return "$sub1" + - "$center" + - "$sub2" - } + private fun String.colorTextForHtml(textColor: String) = "$this" } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_read.xml b/app/src/main/res/layout/activity_book_read.xml index 235c04b87..703c0ef5e 100644 --- a/app/src/main/res/layout/activity_book_read.xml +++ b/app/src/main/res/layout/activity_book_read.xml @@ -37,6 +37,12 @@ android:layout_height="match_parent" android:visibility="gone" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index fc76b9c25..0a001edb6 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -902,5 +902,6 @@ 登录头 字体大小 当前字体大小:%.1f + search result diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 3db3094d9..980799757 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -906,5 +906,6 @@ 登录头 字体大小 当前字体大小:%.1f + search result diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 497af4b2b..9cdec5e98 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -906,5 +906,6 @@ 登录头 字体大小 当前字体大小:%.1f + search result diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 179177f1d..0e21cab6e 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -903,5 +903,6 @@ 登录头 字体大小 当前字体大小:%.1f + 搜索結果 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 0c6b605d1..298e45bc8 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -905,5 +905,6 @@ 登录头 字体大小 当前字体大小:%.1f + 搜索結果 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2d865a2a2..d00b2ac27 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -776,6 +776,7 @@ 切换默认主题 更新时间排序 全文搜索 + 搜索结果 关注公众号[开源阅读]获取订阅源! 当前没有发现源,关注公众号[开源阅读]添加带发现的书源! 将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 681ac87ca..7b096fba9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -906,5 +906,6 @@ login header font scale font scale:%.1f + search result