Merge pull request #374 from h11128/h11128pull

正文搜索功能实现
pull/375/head
kunfei 4 years ago committed by GitHub
commit 1bfd9fa755
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      app/src/main/AndroidManifest.xml
  2. 3
      app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt
  3. 1
      app/src/main/java/io/legado/app/help/BookHelp.kt
  4. 48
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  5. 48
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  6. 7
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  7. 93
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListActivity.kt
  8. 51
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListAdapter.kt
  9. 239
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListFragment.kt
  10. 33
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListViewModel.kt
  11. 40
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt
  12. 19
      app/src/main/res/layout/activity_search_list.xml
  13. 66
      app/src/main/res/layout/fragment_search_list.xml
  14. 19
      app/src/main/res/layout/item_search_list.xml
  15. 39
      app/src/main/res/layout/view_read_menu.xml
  16. 1
      app/src/main/res/values-zh-rHK/strings.xml
  17. 1
      app/src/main/res/values-zh-rTW/strings.xml
  18. 1
      app/src/main/res/values-zh/strings.xml
  19. 1
      app/src/main/res/values/strings.xml

@ -223,6 +223,10 @@
android:name=".ui.book.chapterlist.ChapterListActivity" android:name=".ui.book.chapterlist.ChapterListActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="behind" /> android:screenOrientation="behind" />
<activity
android:name=".ui.book.searchContent.SearchListActivity"
android:launchMode="singleTop"
android:screenOrientation="behind" />
<!-- RSS条目 --> <!-- RSS条目 -->
<activity <activity
android:name=".ui.rss.article.RssSortActivity" android:name=".ui.rss.article.RssSortActivity"

@ -25,6 +25,9 @@ interface BookChapterDao {
@Query("select * from chapters where bookUrl = :bookUrl and `index` = :index") @Query("select * from chapters where bookUrl = :bookUrl and `index` = :index")
fun getChapter(bookUrl: String, index: Int): BookChapter? fun getChapter(bookUrl: String, index: Int): BookChapter?
@Query("select * from chapters where bookUrl = :bookUrl and `title` = :title")
fun getChapter(bookUrl: String, title: String): BookChapter?
@Query("select count(url) from chapters where bookUrl = :bookUrl") @Query("select count(url) from chapters where bookUrl = :bookUrl")
fun getChapterCount(bookUrl: String): Int fun getChapterCount(bookUrl: String): Int

@ -145,6 +145,7 @@ object BookHelp {
return fileNameList return fileNameList
} }
// 检测该章节是否下载
fun hasContent(book: Book, bookChapter: BookChapter): Boolean { fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
return if (book.isLocalBook()) { return if (book.isLocalBook()) {
true true

@ -1,5 +1,6 @@
package io.legado.app.service.help package io.legado.app.service.help
import android.util.Log
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.hankcs.hanlp.HanLP import com.hankcs.hanlp.HanLP
import io.legado.app.App import io.legado.app.App
@ -16,6 +17,7 @@ import io.legado.app.help.coroutine.Coroutine
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.TextChapter import io.legado.app.ui.book.read.page.entities.TextChapter
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.book.read.page.provider.ChapterProvider
import io.legado.app.ui.book.read.page.provider.ImageProvider import io.legado.app.ui.book.read.page.provider.ImageProvider
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -324,6 +326,51 @@ object ReadBook {
} }
} }
fun searchResultPositions(pages: List<TextPage>, indexWithinChapter: Int, query: String): Array<Int>{
//
// calculate search result's pageIndex
var content = ""
pages.map{
content+= it.text
}
var count = 1
var index = content.indexOf(query)
while(count != indexWithinChapter){
index = content.indexOf(query, index + 1);
count += 1
}
val contentPosition = index
var pageIndex = 0
var length = pages[pageIndex].text.length
while (length < contentPosition){
pageIndex += 1
if (pageIndex >pages.size){
pageIndex = pages.size
break
}
length += pages[pageIndex].text.length
}
// calculate search result's lineIndex
val currentPage = pages[pageIndex]
var lineIndex = 0
length = length - currentPage.text.length + currentPage.textLines[lineIndex].text.length
while (length < contentPosition){
lineIndex += 1
if (lineIndex >currentPage.textLines.size){
lineIndex = currentPage.textLines.size
break
}
length += currentPage.textLines[lineIndex].text.length
}
// charIndex
val currentLine = currentPage.textLines[lineIndex]
length -= currentLine.text.length
val charIndex = contentPosition - length
return arrayOf(pageIndex, lineIndex, charIndex)
}
/** /**
* 内容加载完成 * 内容加载完成
*/ */
@ -426,4 +473,5 @@ object ReadBook {
fun pageChanged() fun pageChanged()
fun contentLoadFinish() fun contentLoadFinish()
} }
} }

@ -9,6 +9,7 @@ import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.util.Log
import android.view.* import android.view.*
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.core.view.get import androidx.core.view.get
@ -47,6 +48,8 @@ import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.TextPageFactory import io.legado.app.ui.book.read.page.TextPageFactory
import io.legado.app.ui.book.read.page.delegate.PageDelegate import io.legado.app.ui.book.read.page.delegate.PageDelegate
import io.legado.app.ui.book.searchContent.SearchListActivity
import io.legado.app.ui.book.searchContent.SearchResult
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.login.SourceLogin import io.legado.app.ui.login.SourceLogin
import io.legado.app.ui.replacerule.ReplaceRuleActivity import io.legado.app.ui.replacerule.ReplaceRuleActivity
@ -57,6 +60,9 @@ import kotlinx.android.synthetic.main.activity_book_read.*
import kotlinx.android.synthetic.main.view_read_menu.* import kotlinx.android.synthetic.main.view_read_menu.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
@ -79,6 +85,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
private val requestCodeChapterList = 568 private val requestCodeChapterList = 568
private val requestCodeEditSource = 111 private val requestCodeEditSource = 111
private val requestCodeReplace = 312 private val requestCodeReplace = 312
private val requestCodeSearchResult = 123
private var menu: Menu? = null private var menu: Menu? = null
private var textActionMenu: TextActionMenu? = null private var textActionMenu: TextActionMenu? = null
@ -96,6 +103,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override var isAutoPage = false override var isAutoPage = false
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
override val pageFactory: TextPageFactory get() = page_view.pageFactory override val pageFactory: TextPageFactory get() = page_view.pageFactory
override val headerHeight: Int get() = page_view.curPage.headerHeight override val headerHeight: Int get() = page_view.curPage.headerHeight
@ -532,6 +540,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
intent.removeExtra("readAloud") intent.removeExtra("readAloud")
ReadBook.readAloud() ReadBook.readAloud()
} }
loadStates = true
} }
/** /**
@ -543,6 +552,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
page_view.upContent(relativePosition, resetPageOffset) page_view.upContent(relativePosition, resetPageOffset)
seek_read_page.progress = ReadBook.durPageIndex seek_read_page.progress = ReadBook.durPageIndex
} }
loadStates = false
} }
/** /**
@ -667,6 +677,19 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
} }
} }
/**
* 打开搜索界面
*/
//todo: change request code
override fun openSearchList() {
ReadBook.book?.let {
startActivityForResult<SearchListActivity>(
requestCodeSearchResult,
Pair("bookUrl", it.bookUrl)
)
}
}
/** /**
* 替换规则变化 * 替换规则变化
*/ */
@ -747,11 +770,36 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
requestCodeChapterList -> requestCodeChapterList ->
data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index -> data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index ->
if (index != ReadBook.durChapterIndex) { if (index != ReadBook.durChapterIndex) {
val pageIndex = data.getIntExtra("pageIndex", 0)
viewModel.openChapter(index, pageIndex)
}
}
requestCodeSearchResult ->
data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index ->
launch(IO){
val indexWithinChapter = data.getIntExtra("indexWithinChapter", 0)
val query = data.getStringExtra("query")
viewModel.openChapter(index) viewModel.openChapter(index)
// block until load correct chapter and pages
var pages = ReadBook.curTextChapter?.pages
while (ReadBook.durChapterIndex != index || pages == null ){
delay(100L)
pages = ReadBook.curTextChapter?.pages
}
val positions = ReadBook.searchResultPositions(pages, indexWithinChapter, query!!)
//todo: show selected text
val job1 = async(Main){
ReadBook.skipToPage(positions[0])
page_view.curPage.selectStartMoveIndex(positions[0], positions[1], 0)
page_view.curPage.selectEndMoveIndex(positions[0], positions[1], 0 + query.length )
page_view.isTextSelected = true
}
job1.await()
} }
} }
requestCodeReplace -> onReplaceRuleSave() requestCodeReplace -> onReplaceRuleSave()
} }
} }
} }

@ -200,6 +200,12 @@ class ReadMenu : FrameLayout {
} }
} }
ll_search.onClick {
runMenuOut {
callBack?.openSearchList()
}
}
//朗读 //朗读
ll_read_aloud.onClick { ll_read_aloud.onClick {
runMenuOut { runMenuOut {
@ -291,6 +297,7 @@ class ReadMenu : FrameLayout {
fun autoPage() fun autoPage()
fun openReplaceRule() fun openReplaceRule()
fun openChapterList() fun openChapterList()
fun openSearchList()
fun showReadStyle() fun showReadStyle()
fun showMoreSetting() fun showMoreSetting()
fun showReadAloudDialog() fun showReadAloudDialog()

@ -0,0 +1,93 @@
package io.legado.app.ui.book.searchContent
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 io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.primaryTextColor
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.*
class SearchListActivity : VMBaseActivity<SearchListViewModel>(R.layout.activity_search_list) {
// todo: 完善搜索界面UI
override val viewModel: SearchListViewModel
get() = getViewModel(SearchListViewModel::class.java)
private var searchView: SearchView? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
tab_layout.isTabIndicatorFullWidth = false
tab_layout.setSelectedTabIndicatorColor(accentColor)
intent.getStringExtra("bookUrl")?.let {
viewModel.initBook(it) {
view_pager.adapter = TabFragmentPageAdapter(supportFragmentManager)
tab_layout.setupWithViewPager(view_pager)
}
}
}
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 {
override fun onQueryTextSubmit(query: String): Boolean {
if (viewModel.lastQuery != query){
viewModel.startContentSearch(query)
}
return false
}
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()
}
override fun getCount(): Int {
return 1
}
override fun getPageTitle(position: Int): CharSequence? {
return "Search"
}
}
override fun onBackPressed() {
if (tab_layout.isGone) {
searchView?.onActionViewCollapsed()
tab_layout.visible()
} else {
super.onBackPressed()
}
}
}

@ -0,0 +1,51 @@
package io.legado.app.ui.book.searchContent
import android.content.Context
import android.os.Build
import android.text.Html
import android.util.Log
import android.view.View
import androidx.annotation.RequiresApi
import androidx.core.text.HtmlCompat
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
import io.legado.app.lib.theme.accentColor
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_bookmark.view.*
import kotlinx.android.synthetic.main.item_search_list.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
class SearchListAdapter(context: Context, val callback: Callback) :
SimpleRecyclerAdapter<SearchResult>(context, R.layout.item_search_list) {
val cacheFileNames = hashSetOf<String>()
override fun convert(holder: ItemViewHolder, item: SearchResult, payloads: MutableList<Any>) {
with(holder.itemView) {
val isDur = callback.durChapterIndex() == item.chapterIndex
if (payloads.isEmpty()) {
tv_search_result.text = item.parseText(item.presentText)
if (isDur){
tv_search_result.paint.isFakeBoldText = true
}
}
}
}
override fun registerListener(holder: ItemViewHolder) {
holder.itemView.onClick {
getItem(holder.layoutPosition)?.let {
callback.openSearchResult(it)
}
}
}
interface Callback {
fun openSearchResult(searchResult: SearchResult)
fun durChapterIndex(): Int
}
}

@ -0,0 +1,239 @@
package io.legado.app.ui.book.searchContent
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.lifecycle.LiveData
import com.hankcs.hanlp.HanLP
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseFragment
import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.widget.recycler.UpLinearLayoutManager
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.ColorUtils
import io.legado.app.utils.getViewModelOfActivity
import io.legado.app.utils.observeEvent
import kotlinx.android.synthetic.main.fragment_search_list.*
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import org.jetbrains.anko.sdk27.listeners.onClick
import java.util.regex.Pattern
class SearchListFragment : VMBaseFragment<SearchListViewModel>(R.layout.fragment_search_list),
SearchListAdapter.Callback,
SearchListViewModel.SearchListCallBack{
override val viewModel: SearchListViewModel
get() = getViewModelOfActivity(SearchListViewModel::class.java)
lateinit var adapter: SearchListAdapter
private lateinit var mLayoutManager: UpLinearLayoutManager
private var searchResultCounts = 0
private var durChapterIndex = 0
private var searchResultList: MutableList<SearchResult> = mutableListOf()
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
viewModel.searchCallBack = this
val bbg = bottomBackground
val btc = requireContext().getPrimaryTextColor(ColorUtils.isColorLight(bbg))
ll_search_base_info.setBackgroundColor(bbg)
tv_current_search_info.setTextColor(btc)
iv_search_content_top.setColorFilter(btc)
iv_search_content_bottom.setColorFilter(btc)
initRecyclerView()
initView()
initBook()
}
private fun initRecyclerView() {
adapter = SearchListAdapter(requireContext(), this)
mLayoutManager = UpLinearLayoutManager(requireContext())
recycler_view.layoutManager = mLayoutManager
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
}
private fun initView() {
iv_search_content_top.onClick { mLayoutManager.scrollToPositionWithOffset(0, 0) }
iv_search_content_bottom.onClick {
if (adapter.itemCount > 0) {
mLayoutManager.scrollToPositionWithOffset(adapter.itemCount - 1, 0)
}
}
}
@SuppressLint("SetTextI18n")
private fun initBook() {
launch {
tv_current_search_info.text = "搜索结果:$searchResultCounts"
viewModel.book?.let {
initCacheFileNames(it)
durChapterIndex = it.durChapterIndex
}
}
}
private fun initCacheFileNames(book: Book) {
launch(IO) {
adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book))
withContext(Main) {
adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true)
}
}
}
override fun observeLiveBus() {
observeEvent<BookChapter>(EventBus.SAVE_CONTENT) { chapter ->
viewModel.book?.bookUrl?.let { bookUrl ->
if (chapter.bookUrl == bookUrl) {
adapter.cacheFileNames.add(BookHelp.formatChapterName(chapter))
adapter.notifyItemChanged(chapter.index, true)
}
}
}
}
@SuppressLint("SetTextI18n")
override fun startContentSearch(newText: String) {
// 按章节搜索内容
if (!newText.isBlank()) {
adapter.clearItems()
searchResultList.clear()
searchResultCounts = 0
viewModel.lastQuery = newText
var searchResults = listOf<SearchResult>()
launch(Main){
App.db.bookChapterDao().getChapterList(viewModel.bookUrl).map{ chapter ->
val job = async(IO){
if (isLocalBook || adapter.cacheFileNames.contains(BookHelp.formatChapterName(chapter))) {
searchResults = searchChapter(newText, chapter)
}
}
job.await()
if (searchResults.isNotEmpty()){
searchResultList.addAll(searchResults)
tv_current_search_info.text = "搜索结果:$searchResultCounts"
adapter.addItems(searchResults)
searchResults = listOf<SearchResult>()
}
}
}
}
}
private suspend fun searchChapter(query: String, chapter: BookChapter?): List<SearchResult> {
val searchResults: MutableList<SearchResult> = mutableListOf()
var positions : List<Int> = listOf()
var replaceContents: List<String>? = null
var totalContents = ""
if (chapter != null){
viewModel.book?.let { bookSource ->
val bookContent = BookHelp.getContent(bookSource, chapter)
if (bookContent != null){
//搜索替换后的正文
val job = async(IO) {
chapter.title = when (AppConfig.chineseConverterType) {
1 -> HanLP.convertToSimplifiedChinese(chapter.title)
2 -> HanLP.convertToTraditionalChinese(chapter.title)
else -> chapter.title
}
replaceContents = BookHelp.disposeContent(
chapter.title,
bookSource.name,
bookSource.bookUrl,
bookContent,
bookSource.useReplaceRule
)
}
job.await()
while (replaceContents == null){
delay(100L)
}
totalContents = replaceContents!!.joinToString("")
positions = searchPosition(totalContents, query)
var count = 1
positions.map{
val construct = constructText(totalContents, it, query)
val result = SearchResult(index = searchResultCounts,
indexWithinChapter = count,
text = construct[1] as String,
chapterTitle = chapter.title,
query = query,
chapterIndex = chapter.index,
newPosition = construct[0] as Int,
contentPosition = it
)
count += 1
searchResultCounts += 1
searchResults.add(result)
}
}
}
}
return searchResults
}
private fun searchPosition(content: String, pattern: String): List<Int> {
val position : MutableList<Int> = mutableListOf()
var index = content.indexOf(pattern)
while(index >= 0){
position.add(index)
index = content.indexOf(pattern, index + 1);
}
return position
}
private fun constructText(content: String, position: Int, query: String): Array<Any>{
// 构建关键词周边文字,在搜索结果里显示
// todo: 判断段落,只在关键词所在段落内分割
// todo: 利用标点符号分割完整的句
// todo: length和设置结合,自由调整周边文字长度
val length = 20
var po1 = position - length
var po2 = position + query.length + length
if (po1 <0) {
po1 = 0
}
if (po2 > content.length){
po2 = content.length
}
val newPosition = position - po1
val newText = content.substring(po1, po2)
return arrayOf(newPosition, newText)
}
val isLocalBook: Boolean
get() = viewModel.book?.isLocalBook() == true
override fun openSearchResult(searchResult: SearchResult) {
val searchData = Intent()
searchData.putExtra("index", searchResult.chapterIndex)
searchData.putExtra("contentPosition", searchResult.contentPosition)
searchData.putExtra("query", searchResult.query)
searchData.putExtra("indexWithinChapter", searchResult.indexWithinChapter)
activity?.setResult(RESULT_OK, searchData)
activity?.finish()
}
override fun durChapterIndex(): Int {
return durChapterIndex
}
}

@ -0,0 +1,33 @@
package io.legado.app.ui.book.searchContent
import android.app.Application
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.Book
class SearchListViewModel(application: Application) : BaseViewModel(application) {
var bookUrl: String = ""
var book: Book? = null
var searchCallBack: SearchListCallBack? = null
var lastQuery: String = ""
fun initBook(bookUrl: String, success: () -> Unit) {
this.bookUrl = bookUrl
execute {
book = App.db.bookDao().getBook(bookUrl)
}.onSuccess {
success.invoke()
}
}
fun startContentSearch(newText: String) {
searchCallBack?.startContentSearch(newText)
}
interface SearchListCallBack {
fun startContentSearch(newText: String)
}
}

@ -0,0 +1,40 @@
package io.legado.app.ui.book.searchContent
import android.text.Spanned
import android.util.Log
import androidx.core.text.HtmlCompat
import io.legado.app.ui.book.read.page.entities.TextPage
data class SearchResult(
var index: Int = 0,
var indexWithinChapter: Int = 0,
var text: String = "",
var 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 presentText: String
get(){
return colorPresentText(newPosition, query, text) +
"<font color=#0000ff>($chapterTitle)</font>"
}
fun colorPresentText(position: Int, center: String, targetText: String): String{
val sub1 = text.substring(0, position)
val sub2 = text.substring(position + center.length, targetText.length)
return "<font color=#000000>$sub1</font>" +
"<font color=#ff0000>$center</font>" +
"<font color=#000000>$sub2</font>"
}
fun parseText(targetText: String): Spanned {
return HtmlCompat.fromHtml(targetText, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<io.legado.app.ui.widget.TitleBar
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentLayout="@layout/view_tab_layout"/>
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants">
<io.legado.app.ui.widget.recycler.scroller.FastScrollRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:overScrollMode="never"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/ll_search_base_info" />
<LinearLayout
android:id="@+id/ll_search_base_info"
android:layout_width="match_parent"
android:layout_height="36dp"
android:background="@color/background"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:elevation="5dp"
android:gravity="center_vertical"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/tv_current_search_info"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:ellipsize="middle"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:singleLine="true"
android:gravity="center_vertical"
android:textColor="@color/primaryText"
android:textSize="12sp" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_search_content_top"
android:layout_width="36dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/go_to_top"
android:src="@drawable/ic_arrow_drop_up"
android:tooltipText="@string/go_to_top"
app:tint="@color/primaryText"
tools:ignore="UnusedAttribute" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_search_content_bottom"
android:layout_width="36dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/go_to_bottom"
android:src="@drawable/ic_arrow_drop_down"
android:tooltipText="@string/go_to_bottom"
app:tint="@color/primaryText"
tools:ignore="UnusedAttribute" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,19 @@
<?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"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:padding="12dp">
<TextView
android:id="@+id/tv_search_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="false"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -260,6 +260,45 @@
android:textSize="12sp" /> android:textSize="12sp" />
</LinearLayout> </LinearLayout>
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2" />
<!--搜索按钮-->
<LinearLayout
android:id="@+id/ll_search"
android:layout_width="60dp"
android:layout_height="50dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/search_content"
android:focusable="true"
android:orientation="vertical"
android:paddingBottom="7dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_search"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:contentDescription="@string/search_content"
android:src="@drawable/ic_search"
app:tint="@color/primaryText"
tools:ignore="NestedWeights" />
<TextView
android:id="@+id/tv_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="3dp"
android:maxLines="1"
android:text="@string/search_content"
android:textColor="@color/primaryText"
android:textSize="12sp" />
</LinearLayout>
<View <View
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"

@ -761,5 +761,6 @@
<string name="select_theme">切換默認主題</string> <string name="select_theme">切換默認主題</string>
<string name="share_selected_source">分享選中書源</string> <string name="share_selected_source">分享選中書源</string>
<string name="sort_by_lastUppdateTime">時間排序</string> <string name="sort_by_lastUppdateTime">時間排序</string>
<string name="search_content">搜索</string>
</resources> </resources>

@ -761,6 +761,7 @@
<string name="select_theme">切換默認主題</string> <string name="select_theme">切換默認主題</string>
<string name="share_selected_source">分享選中書源</string> <string name="share_selected_source">分享選中書源</string>
<string name="sort_by_lastUppdateTime">時間排序</string> <string name="sort_by_lastUppdateTime">時間排序</string>
<string name="search_content">搜索</string>
</resources> </resources>

@ -761,5 +761,6 @@
<string name="theme_list_summary">使用保存主题,导入,分享主题</string> <string name="theme_list_summary">使用保存主题,导入,分享主题</string>
<string name="select_theme">切换默认主题</string> <string name="select_theme">切换默认主题</string>
<string name="sort_by_lastUppdateTime">时间排序</string> <string name="sort_by_lastUppdateTime">时间排序</string>
<string name="search_content">搜索</string>
</resources> </resources>

@ -763,5 +763,6 @@
<string name="theme_list_summary">Save, Import, Share theme</string> <string name="theme_list_summary">Save, Import, Share theme</string>
<string name="share_selected_source">Share selected sources</string> <string name="share_selected_source">Share selected sources</string>
<string name="sort_by_lastUppdateTime">Sort by update time</string> <string name="sort_by_lastUppdateTime">Sort by update time</string>
<string name="search_content">Search</string>
</resources> </resources>
Loading…
Cancel
Save