pull/395/head
Celeter 4 years ago
commit 9c81d03788
  1. 6
      app/src/main/AndroidManifest.xml
  2. 8
      app/src/main/assets/txtTocRule.json
  3. 4
      app/src/main/assets/updateLog.md
  4. 2
      app/src/main/java/io/legado/app/App.kt
  5. 3
      app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt
  6. 1
      app/src/main/java/io/legado/app/help/BookHelp.kt
  7. 37
      app/src/main/java/io/legado/app/help/IntentHelp.kt
  8. 32
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  9. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  10. 35
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  11. 197
      app/src/main/java/io/legado/app/service/DownloadService.kt
  12. 26
      app/src/main/java/io/legado/app/service/help/Download.kt
  13. 48
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  14. 48
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  15. 7
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  16. 2
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  17. 93
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListActivity.kt
  18. 51
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListAdapter.kt
  19. 239
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListFragment.kt
  20. 33
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchListViewModel.kt
  21. 43
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt
  22. 5
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  23. 6
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  24. 19
      app/src/main/res/layout/activity_search_list.xml
  25. 66
      app/src/main/res/layout/fragment_search_list.xml
  26. 19
      app/src/main/res/layout/item_search_list.xml
  27. 39
      app/src/main/res/layout/view_read_menu.xml
  28. 1
      app/src/main/res/values-zh-rHK/strings.xml
  29. 1
      app/src/main/res/values-zh-rTW/strings.xml
  30. 1
      app/src/main/res/values-zh/strings.xml
  31. 1
      app/src/main/res/values/strings.xml

@ -18,6 +18,7 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<application
android:name=".App"
@ -222,6 +223,10 @@
android:name=".ui.book.chapterlist.ChapterListActivity"
android:launchMode="singleTop"
android:screenOrientation="behind" />
<activity
android:name=".ui.book.searchContent.SearchListActivity"
android:launchMode="singleTop"
android:screenOrientation="behind" />
<!-- RSS条目 -->
<activity
android:name=".ui.rss.article.RssSortActivity"
@ -354,6 +359,7 @@
<service android:name=".service.TTSReadAloudService" />
<service android:name=".service.HttpReadAloudService" />
<service android:name=".service.AudioPlayService" />
<service android:name=".service.DownloadService" />
<receiver android:name=".receiver.MediaButtonReceiver">
<intent-filter>

@ -2,15 +2,15 @@
{
"id": -1,
"enable": true,
"name": "目录",
"rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$",
"name": "目录(去空白)",
"rule": "(?<=[ \\s])(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$",
"serialNumber": 0
},
{
"id": -2,
"enable": true,
"name": "目录(去空白)",
"rule": "(?<=[ \\s])(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$",
"name": "目录",
"rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$",
"serialNumber": 1
},
{

@ -3,6 +3,10 @@
* 关注合作公众号 **[小说拾遗]()** 获取好看的小说。
- 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/09/15**
* 修复导入排版字体重复报错的bug
* 添加正文搜索 by [h11128](https://github.com/h11128)
**2020/09/12**
* web看书同步最新章
* web写源增加图片样式等规则

@ -156,7 +156,7 @@ class App : MultiDexApplication() {
//用唯一的ID创建渠道对象
val downloadChannel = NotificationChannel(
channelIdDownload,
getString(R.string.offline_cache),
getString(R.string.action_download),
NotificationManager.IMPORTANCE_LOW
)
//初始化channel

@ -25,6 +25,9 @@ interface BookChapterDao {
@Query("select * from chapters where bookUrl = :bookUrl and `index` = :index")
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")
fun getChapterCount(bookUrl: String): Int

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

@ -3,6 +3,7 @@ package io.legado.app.help
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import io.legado.app.R
import org.jetbrains.anko.toast
@ -32,21 +33,29 @@ object IntentHelp {
}
}
inline fun <reified T> servicePendingIntent(context: Context, action: String): PendingIntent? {
return PendingIntent.getService(
context,
0,
Intent(context, T::class.java).apply { this.action = action },
PendingIntent.FLAG_UPDATE_CURRENT
)
inline fun <reified T> servicePendingIntent(
context: Context,
action: String,
bundle: Bundle? = null
): PendingIntent? {
val intent = Intent(context, T::class.java)
intent.action = action
bundle?.let {
intent.putExtras(bundle)
}
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
inline fun <reified T> activityPendingIntent(context: Context, action: String): PendingIntent? {
return PendingIntent.getActivity(
context,
0,
Intent(context, T::class.java).apply { this.action = action },
PendingIntent.FLAG_UPDATE_CURRENT
)
inline fun <reified T> activityPendingIntent(
context: Context,
action: String,
bundle: Bundle? = null
): PendingIntent? {
val intent = Intent(context, T::class.java)
intent.action = action
bundle?.let {
intent.putExtras(bundle)
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}

@ -324,17 +324,37 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
*/
private fun replaceRegex(result: String, rule: SourceRule): String {
var vResult = result
if (rule.replaceRegex.isNotEmpty()) {
val stringBuffer = StringBuffer()
val evalMatcher = replacePattern.matcher(rule.replaceRegex)
while (evalMatcher.find()) {
val jsEval = evalMatcher.group().let {
if (it.startsWith("@get:", true)) {
get(it.substring(6, it.lastIndex))
} else {
evalJS(it.substring(2, it.length - 2), result)
}
} ?: ""
if (jsEval is String) {
evalMatcher.appendReplacement(stringBuffer, jsEval)
} else if (jsEval is Double && jsEval % 1.0 == 0.0) {
evalMatcher.appendReplacement(stringBuffer, String.format("%.0f", jsEval))
} else {
evalMatcher.appendReplacement(stringBuffer, jsEval.toString())
}
}
evalMatcher.appendTail(stringBuffer)
val replaceRegex = stringBuffer.toString()
if (replaceRegex.isNotEmpty()) {
vResult = if (rule.replaceFirst) {
val pattern = Pattern.compile(rule.replaceRegex)
val pattern = Pattern.compile(replaceRegex)
val matcher = pattern.matcher(vResult)
if (matcher.find()) {
matcher.group(0)!!.replaceFirst(rule.replaceRegex.toRegex(), rule.replacement)
matcher.group(0)!!.replaceFirst(replaceRegex.toRegex(), rule.replacement)
} else {
""
}
} else {
vResult.replace(rule.replaceRegex.toRegex(), rule.replacement)
vResult.replace(replaceRegex.toRegex(), rule.replacement)
}
}
return vResult
@ -644,6 +664,10 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
"@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}|\\$\\d{1,2}",
Pattern.CASE_INSENSITIVE
)
private val replacePattern = Pattern.compile(
"@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}",
Pattern.CASE_INSENSITIVE
)
}
}

@ -150,7 +150,7 @@ class AnalyzeUrl(
//js
if (ruleUrl.contains("{{") && ruleUrl.contains("}}")) {
var jsEval: Any
val sb = StringBuffer(ruleUrl.length)
val sb = StringBuffer()
val simpleBindings = SimpleBindings()
simpleBindings["java"] = this
simpleBindings["baseUrl"] = baseUrl

@ -24,6 +24,21 @@ class CheckSourceService : BaseService() {
private val allIds = ArrayList<String>()
private val checkedIds = ArrayList<String>()
private var processIndex = 0
private val notificationBuilder by lazy {
NotificationCompat.Builder(this, AppConst.channelIdReadAloud)
.setSmallIcon(R.drawable.ic_network_check)
.setOngoing(true)
.setContentTitle(getString(R.string.check_book_source))
.setContentIntent(
IntentHelp.activityPendingIntent<BookSourceActivity>(this, "activity")
)
.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.cancel),
IntentHelp.servicePendingIntent<CheckSourceService>(this, IntentAction.stop)
)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
}
override fun onCreate() {
super.onCreate()
@ -105,23 +120,9 @@ class CheckSourceService : BaseService() {
* 更新通知
*/
private fun updateNotification(state: Int, msg: String) {
val builder = NotificationCompat.Builder(this, AppConst.channelIdReadAloud)
.setSmallIcon(R.drawable.ic_network_check)
.setOngoing(true)
.setContentTitle(getString(R.string.check_book_source))
.setContentText(msg)
.setContentIntent(
IntentHelp.activityPendingIntent<BookSourceActivity>(this, "activity")
)
.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.cancel),
IntentHelp.servicePendingIntent<CheckSourceService>(this, IntentAction.stop)
)
builder.setProgress(allIds.size, state, false)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notification = builder.build()
startForeground(112202, notification)
notificationBuilder.setContentText(msg)
notificationBuilder.setProgress(allIds.size, state, false)
startForeground(112202, notificationBuilder.build())
}
}

@ -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)
}
}
}

@ -1,5 +1,6 @@
package io.legado.app.service.help
import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.hankcs.hanlp.HanLP
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.service.BaseReadAloudService
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.ImageProvider
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 contentLoadFinish()
}
}

@ -9,6 +9,7 @@ import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.*
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
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.TextPageFactory
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.login.SourceLogin
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.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.startActivity
@ -79,6 +85,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
private val requestCodeChapterList = 568
private val requestCodeEditSource = 111
private val requestCodeReplace = 312
private val requestCodeSearchResult = 123
private var menu: Menu? = null
private var textActionMenu: TextActionMenu? = null
@ -96,6 +103,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override var isAutoPage = false
private var screenTimeOut: Long = 0
private var timeBatteryReceiver: TimeBatteryReceiver? = null
private var loadStates: Boolean = false
override val pageFactory: TextPageFactory get() = page_view.pageFactory
override val headerHeight: Int get() = page_view.curPage.headerHeight
@ -532,6 +540,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
intent.removeExtra("readAloud")
ReadBook.readAloud()
}
loadStates = true
}
/**
@ -543,6 +552,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
page_view.upContent(relativePosition, resetPageOffset)
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 ->
data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index ->
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)
// 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()
}
}
}

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

@ -324,7 +324,9 @@ class BgTextConfigDialog : BaseDialogFragment(), FileChooserDialog.CallBack {
val fontName = FileUtils.getName(config.textFont)
val fontPath =
FileUtils.getPath(requireContext().externalFilesDir, "font", fontName)
if (!FileUtils.exist(fontPath)) {
FileUtils.getFile(configDir, fontName).copyTo(File(fontPath))
}
config.textFont = fontPath
}
if (config.bgType() == 2) {

@ -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)
}
}

@ -24,9 +24,6 @@ import io.legado.app.constant.AppPattern
import io.legado.app.data.entities.BookSource
import io.legado.app.help.IntentDataHelp
import io.legado.app.lib.dialogs.*
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.service.help.CheckSource
@ -47,7 +44,6 @@ import kotlinx.android.synthetic.main.view_search.*
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.startActivityForResult
import org.jetbrains.anko.toast
import java.io.File
import java.text.Collator
@ -212,7 +208,6 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
else -> data.reversed()
}
}
recycler_view.scrollToPosition(0)
val diffResult = DiffUtil
.calculateDiff(DiffCallBack(ArrayList(adapter.getItems()), sourceList))
adapter.setItems(sourceList, diffResult)

@ -15,12 +15,14 @@ import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.lib.theme.DrawableUtils
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.service.help.Download
import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.filechooser.FilePicker
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_rss_read.*
import kotlinx.coroutines.launch
import org.apache.commons.text.StringEscapeUtils
import org.jetbrains.anko.downloadManager
import org.jetbrains.anko.share
import org.jsoup.Jsoup
@ -160,12 +162,12 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
request.setAllowedOverRoaming(true)
// 允许下载的网路类型
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
// 设置下载文件保存的路径和文件名
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
// 添加一个下载任务
val downloadId = downloadManager.enqueue(request)
print(downloadId)
Download.start(this, downloadId, fileName)
}
}
}

@ -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" />
</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
android:layout_width="0dp"
android:layout_height="match_parent"

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

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

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

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