Merge branch 'master' of https://github.com/Celeter/legado
commit
9c81d03788
@ -0,0 +1,197 @@ |
|||||||
|
package io.legado.app.service |
||||||
|
|
||||||
|
import android.app.DownloadManager |
||||||
|
import android.content.BroadcastReceiver |
||||||
|
import android.content.Context |
||||||
|
import android.content.Intent |
||||||
|
import android.content.IntentFilter |
||||||
|
import android.net.Uri |
||||||
|
import android.os.Build |
||||||
|
import android.os.Handler |
||||||
|
import androidx.core.app.NotificationCompat |
||||||
|
import androidx.core.content.FileProvider |
||||||
|
import androidx.core.os.bundleOf |
||||||
|
import io.legado.app.BuildConfig |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.base.BaseService |
||||||
|
import io.legado.app.constant.AppConst |
||||||
|
import io.legado.app.constant.IntentAction |
||||||
|
import io.legado.app.help.IntentHelp |
||||||
|
import io.legado.app.utils.RealPathUtil |
||||||
|
import io.legado.app.utils.msg |
||||||
|
import org.jetbrains.anko.downloadManager |
||||||
|
import org.jetbrains.anko.toast |
||||||
|
import java.io.File |
||||||
|
|
||||||
|
|
||||||
|
class DownloadService : BaseService() { |
||||||
|
|
||||||
|
private val downloads = hashMapOf<Long, String>() |
||||||
|
private val completeDownloads = hashSetOf<Long>() |
||||||
|
private val handler = Handler() |
||||||
|
private val runnable = Runnable { |
||||||
|
checkDownloadState() |
||||||
|
} |
||||||
|
|
||||||
|
private val downloadReceiver = object : BroadcastReceiver() { |
||||||
|
override fun onReceive(context: Context, intent: Intent) { |
||||||
|
queryState() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCreate() { |
||||||
|
super.onCreate() |
||||||
|
registerReceiver(downloadReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onTaskRemoved(rootIntent: Intent?) { |
||||||
|
super.onTaskRemoved(rootIntent) |
||||||
|
stopSelf() |
||||||
|
} |
||||||
|
|
||||||
|
override fun onDestroy() { |
||||||
|
super.onDestroy() |
||||||
|
unregisterReceiver(downloadReceiver) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { |
||||||
|
when (intent?.action) { |
||||||
|
IntentAction.start -> startDownload( |
||||||
|
intent.getLongExtra("downloadId", 0), |
||||||
|
intent.getStringExtra("fileName") ?: "未知文件" |
||||||
|
) |
||||||
|
IntentAction.play -> { |
||||||
|
val id = intent.getLongExtra("downloadId", 0) |
||||||
|
if (downloads[id]?.endsWith(".apk") == true) { |
||||||
|
installApk(id) |
||||||
|
} |
||||||
|
} |
||||||
|
IntentAction.stop -> { |
||||||
|
val downloadId = intent.getLongExtra("downloadId", 0) |
||||||
|
downloads.remove(downloadId) |
||||||
|
if (downloads.isEmpty()) { |
||||||
|
stopSelf() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return super.onStartCommand(intent, flags, startId) |
||||||
|
} |
||||||
|
|
||||||
|
private fun startDownload(downloadId: Long, fileName: String) { |
||||||
|
if (downloadId > 0) { |
||||||
|
downloads[downloadId] = fileName |
||||||
|
queryState() |
||||||
|
checkDownloadState() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun checkDownloadState() { |
||||||
|
handler.removeCallbacks(runnable) |
||||||
|
queryState() |
||||||
|
handler.postDelayed(runnable, 1000) |
||||||
|
} |
||||||
|
|
||||||
|
//查询下载进度 |
||||||
|
private fun queryState() { |
||||||
|
val ids = downloads.keys |
||||||
|
val query = DownloadManager.Query() |
||||||
|
query.setFilterById(*ids.toLongArray()) |
||||||
|
downloadManager.query(query).use { cursor -> |
||||||
|
if (!cursor.moveToFirst()) return |
||||||
|
val id = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID)) |
||||||
|
val progress: Int = |
||||||
|
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) |
||||||
|
val max: Int = |
||||||
|
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) |
||||||
|
val status = |
||||||
|
when (cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))) { |
||||||
|
DownloadManager.STATUS_PAUSED -> "暂停" |
||||||
|
DownloadManager.STATUS_PENDING -> "待下载" |
||||||
|
DownloadManager.STATUS_RUNNING -> "下载中" |
||||||
|
DownloadManager.STATUS_SUCCESSFUL -> { |
||||||
|
if (!completeDownloads.contains(id)) { |
||||||
|
completeDownloads.add(id) |
||||||
|
if (downloads[id]?.endsWith(".apk") == true) { |
||||||
|
installApk(id) |
||||||
|
} |
||||||
|
} |
||||||
|
"下载完成" |
||||||
|
} |
||||||
|
DownloadManager.STATUS_FAILED -> "下载失败" |
||||||
|
else -> "未知状态" |
||||||
|
} |
||||||
|
updateNotification(id, "${downloads[id]} $status", max, progress) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun installApk(downloadId: Long) { |
||||||
|
downloadManager.getUriForDownloadedFile(downloadId)?.let { |
||||||
|
val filePath = RealPathUtil.getPath(this, it) ?: return |
||||||
|
val file = File(filePath) |
||||||
|
//调用系统安装apk |
||||||
|
val intent = Intent() |
||||||
|
intent.action = Intent.ACTION_VIEW |
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //7.0版本以上 |
||||||
|
val uriForFile: Uri = |
||||||
|
FileProvider.getUriForFile( |
||||||
|
this, |
||||||
|
"${BuildConfig.APPLICATION_ID}.fileProvider", |
||||||
|
file |
||||||
|
) |
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) |
||||||
|
intent.setDataAndType(uriForFile, "application/vnd.android.package-archive") |
||||||
|
} else { |
||||||
|
val uri: Uri = Uri.fromFile(file) |
||||||
|
intent.setDataAndType(uri, "application/vnd.android.package-archive") |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
startActivity(intent) |
||||||
|
} catch (e: Exception) { |
||||||
|
toast(e.msg) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 更新通知 |
||||||
|
*/ |
||||||
|
private fun updateNotification(downloadId: Long, content: String, max: Int, progress: Int) { |
||||||
|
val notificationBuilder = NotificationCompat.Builder(this, AppConst.channelIdDownload) |
||||||
|
.setSmallIcon(R.drawable.ic_download) |
||||||
|
.setOngoing(true) |
||||||
|
.setContentTitle(getString(R.string.action_download)) |
||||||
|
notificationBuilder.setContentIntent( |
||||||
|
IntentHelp.servicePendingIntent<DownloadService>( |
||||||
|
this, |
||||||
|
IntentAction.play, |
||||||
|
bundleOf("downloadId" to downloadId) |
||||||
|
) |
||||||
|
) |
||||||
|
notificationBuilder.addAction( |
||||||
|
R.drawable.ic_stop_black_24dp, |
||||||
|
getString(R.string.cancel), |
||||||
|
IntentHelp.servicePendingIntent<DownloadService>( |
||||||
|
this, |
||||||
|
IntentAction.stop, |
||||||
|
bundleOf("downloadId" to downloadId) |
||||||
|
) |
||||||
|
) |
||||||
|
notificationBuilder.setDeleteIntent( |
||||||
|
IntentHelp.servicePendingIntent<DownloadService>( |
||||||
|
this, |
||||||
|
IntentAction.stop, |
||||||
|
bundleOf("downloadId" to downloadId) |
||||||
|
) |
||||||
|
) |
||||||
|
notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) |
||||||
|
notificationBuilder.setContentText(content) |
||||||
|
notificationBuilder.setProgress(max, progress, false) |
||||||
|
notificationBuilder.setAutoCancel(true) |
||||||
|
val notification = notificationBuilder.build() |
||||||
|
startForeground(downloadId.toInt(), notification) |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
package io.legado.app.service.help |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import android.content.Intent |
||||||
|
import io.legado.app.constant.IntentAction |
||||||
|
import io.legado.app.service.DownloadService |
||||||
|
|
||||||
|
object Download { |
||||||
|
|
||||||
|
fun start(context: Context, downloadId: Long, fileName: String) { |
||||||
|
Intent(context, DownloadService::class.java).let { |
||||||
|
it.action = IntentAction.start |
||||||
|
it.putExtra("downloadId", downloadId) |
||||||
|
it.putExtra("fileName", fileName) |
||||||
|
context.startService(it) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun stop(context: Context) { |
||||||
|
Intent(context, DownloadService::class.java).let { |
||||||
|
it.action = IntentAction.stop |
||||||
|
context.startService(it) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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,43 @@ |
|||||||
|
package io.legado.app.ui.book.searchContent |
||||||
|
|
||||||
|
import android.text.Spanned |
||||||
|
import androidx.core.text.HtmlCompat |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.utils.getCompatColor |
||||||
|
import io.legado.app.utils.hexString |
||||||
|
|
||||||
|
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) |
||||||
|
val textColor = App.INSTANCE.getCompatColor(R.color.primaryText).hexString |
||||||
|
return "<font color=#${textColor}>$sub1</font>" + |
||||||
|
"<font color=#ff0000>$center</font>" + |
||||||
|
"<font color=#${textColor}>$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> |
Loading…
Reference in new issue