diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListActivity.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListActivity.kt index ec1cbc0c7..c21f3ce75 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListActivity.kt @@ -1,93 +1,263 @@ 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(R.layout.activity_search_list) { - // todo: 完善搜索界面UI +class SearchListActivity : VMBaseActivity(R.layout.activity_search_list), + SearchListAdapter.Callback, + SearchListViewModel.SearchListCallBack { + 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 = mutableListOf() override fun onActivityCreated(savedInstanceState: Bundle?) { - tab_layout.isTabIndicatorFullWidth = false - tab_layout.setSelectedTabIndicatorColor(accentColor) + viewModel.searchCallBack = this + 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){ + if (viewModel.lastQuery != query) { viewModel.startContentSearch(query) } - return false + return true } - override fun onQueryTextChange(newText: String): Boolean { - + override fun onQueryTextChange(newText: String?): Boolean { return false } }) - return super.onCompatCreateOptionsMenu(menu) } - private inner class TabFragmentPageAdapter(fm: FragmentManager) : - FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - override fun getItem(position: Int): Fragment { - return SearchListFragment() + private fun initRecyclerView() { + adapter = SearchListAdapter(this, this) + mLayoutManager = UpLinearLayoutManager(this) + recycler_view.layoutManager = mLayoutManager + recycler_view.addItemDecoration(VerticalDivider(this)) + recycler_view.adapter = adapter + } + + private fun initView() { + iv_search_content_top.onClick { mLayoutManager.scrollToPositionWithOffset(0, 0) } + iv_search_content_bottom.onClick { + if (adapter.itemCount > 0) { + mLayoutManager.scrollToPositionWithOffset(adapter.itemCount - 1, 0) + } + } + } + + @SuppressLint("SetTextI18n") + private fun initBook() { + launch { + tv_current_search_info.text = "搜索结果:$searchResultCounts" + viewModel.book?.let { + initCacheFileNames(it) + durChapterIndex = it.durChapterIndex + } + } + } + + 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(EventBus.SAVE_CONTENT) { chapter -> + viewModel.book?.bookUrl?.let { bookUrl -> + if (chapter.bookUrl == bookUrl) { + adapter.cacheFileNames.add(BookHelp.formatChapterName(chapter)) + adapter.notifyItemChanged(chapter.index, true) + } + } } + } - override fun getCount(): Int { - return 1 + @SuppressLint("SetTextI18n") + override fun startContentSearch(newText: String) { + // 按章节搜索内容 + if (!newText.isBlank()) { + adapter.clearItems() + searchResultList.clear() + searchResultCounts = 0 + viewModel.lastQuery = newText + var searchResults = listOf() + launch(Dispatchers.Main) { + App.db.bookChapterDao().getChapterList(viewModel.bookUrl).map { chapter -> + val job = async(Dispatchers.IO) { + if (isLocalBook || adapter.cacheFileNames.contains( + BookHelp.formatChapterName( + chapter + ) + ) + ) { + searchResults = searchChapter(newText, chapter) + } + } + job.await() + if (searchResults.isNotEmpty()) { + searchResultList.addAll(searchResults) + tv_current_search_info.text = "搜索结果:$searchResultCounts" + adapter.addItems(searchResults) + searchResults = listOf() + } + } + } } + } - override fun getPageTitle(position: Int): CharSequence? { - return "Search" + private suspend fun searchChapter(query: String, chapter: BookChapter?): List { + val searchResults: MutableList = mutableListOf() + var positions: List + var replaceContents: List? = 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 { + val position: MutableList = mutableListOf() + var index = content.indexOf(pattern) + while (index >= 0) { + position.add(index) + index = content.indexOf(pattern, index + 1) + } + return position } - override fun onBackPressed() { - if (tab_layout.isGone) { - searchView?.onActionViewCollapsed() - tab_layout.visible() - } else { - super.onBackPressed() + private fun constructText(content: String, position: Int, query: String): Array { + // 构建关键词周边文字,在搜索结果里显示 + // 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 + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListFragment.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListFragment.kt deleted file mode 100644 index 38ac013df..000000000 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListFragment.kt +++ /dev/null @@ -1,239 +0,0 @@ -package io.legado.app.ui.book.searchContent - -import android.annotation.SuppressLint -import android.app.Activity.RESULT_OK -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.view.View -import androidx.lifecycle.LiveData -import com.hankcs.hanlp.HanLP -import io.legado.app.App -import io.legado.app.R -import io.legado.app.base.VMBaseFragment -import io.legado.app.constant.EventBus -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookChapter -import io.legado.app.help.AppConfig -import io.legado.app.help.BookHelp -import io.legado.app.lib.theme.bottomBackground -import io.legado.app.lib.theme.getPrimaryTextColor -import io.legado.app.service.help.ReadBook -import io.legado.app.ui.book.read.page.entities.TextPage -import io.legado.app.ui.book.read.page.provider.ChapterProvider -import io.legado.app.ui.widget.recycler.UpLinearLayoutManager -import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.utils.ColorUtils -import io.legado.app.utils.getViewModelOfActivity -import io.legado.app.utils.observeEvent -import kotlinx.android.synthetic.main.fragment_search_list.* -import kotlinx.coroutines.* -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main -import org.jetbrains.anko.sdk27.listeners.onClick -import java.util.regex.Pattern - -class SearchListFragment : VMBaseFragment(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 = 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(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() - launch(Main){ - App.db.bookChapterDao().getChapterList(viewModel.bookUrl).map{ chapter -> - val job = async(IO){ - if (isLocalBook || adapter.cacheFileNames.contains(BookHelp.formatChapterName(chapter))) { - searchResults = searchChapter(newText, chapter) - } - } - job.await() - if (searchResults.isNotEmpty()){ - searchResultList.addAll(searchResults) - tv_current_search_info.text = "搜索结果:$searchResultCounts" - adapter.addItems(searchResults) - searchResults = listOf() - } - } - } - } - } - - private suspend fun searchChapter(query: String, chapter: BookChapter?): List { - val searchResults: MutableList = mutableListOf() - var positions : List = listOf() - var replaceContents: List? = 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 { - 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) { - - val searchData = Intent() - searchData.putExtra("index", searchResult.chapterIndex) - searchData.putExtra("contentPosition", searchResult.contentPosition) - searchData.putExtra("query", searchResult.query) - searchData.putExtra("indexWithinChapter", searchResult.indexWithinChapter) - activity?.setResult(RESULT_OK, searchData) - activity?.finish() - - - } - - override fun durChapterIndex(): Int { - return durChapterIndex - } - -} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search_list.xml b/app/src/main/res/layout/activity_search_list.xml index 159f77324..ce7e57b93 100644 --- a/app/src/main/res/layout/activity_search_list.xml +++ b/app/src/main/res/layout/activity_search_list.xml @@ -1,19 +1,74 @@ - + + android:id="@+id/title_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:contentLayout="@layout/view_search" + app:contentInsetRight="24dp" + app:layout_constraintTop_toTopOf="parent" /> - + - \ No newline at end of file + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_list.xml b/app/src/main/res/layout/fragment_search_list.xml deleted file mode 100644 index 79fefe11f..000000000 --- a/app/src/main/res/layout/fragment_search_list.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file