diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 34c57bb7b..ef2c71ab2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ + + + diff --git a/app/src/main/assets/txtTocRule.json b/app/src/main/assets/txtTocRule.json index 11131b334..127f5648f 100644 --- a/app/src/main/assets/txtTocRule.json +++ b/app/src/main/assets/txtTocRule.json @@ -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 }, { diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md index e9ab31cb0..40348af60 100644 --- a/app/src/main/assets/updateLog.md +++ b/app/src/main/assets/updateLog.md @@ -3,6 +3,10 @@ * 关注合作公众号 **[小说拾遗]()** 获取好看的小说。 - 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 +**2020/09/15** +* 修复导入排版字体重复报错的bug +* 添加正文搜索 by [h11128](https://github.com/h11128) + **2020/09/12** * web看书同步最新章 * web写源增加图片样式等规则 diff --git a/app/src/main/java/io/legado/app/App.kt b/app/src/main/java/io/legado/app/App.kt index 98f255c72..37aea552d 100644 --- a/app/src/main/java/io/legado/app/App.kt +++ b/app/src/main/java/io/legado/app/App.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt index d0588bec8..0041591e3 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/help/BookHelp.kt b/app/src/main/java/io/legado/app/help/BookHelp.kt index 89a2bf41b..f52252f0b 100644 --- a/app/src/main/java/io/legado/app/help/BookHelp.kt +++ b/app/src/main/java/io/legado/app/help/BookHelp.kt @@ -145,6 +145,7 @@ object BookHelp { return fileNameList } + // 检测该章节是否下载 fun hasContent(book: Book, bookChapter: BookChapter): Boolean { return if (book.isLocalBook()) { true diff --git a/app/src/main/java/io/legado/app/help/IntentHelp.kt b/app/src/main/java/io/legado/app/help/IntentHelp.kt index d02e13392..d7ed03af1 100644 --- a/app/src/main/java/io/legado/app/help/IntentHelp.kt +++ b/app/src/main/java/io/legado/app/help/IntentHelp.kt @@ -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 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 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 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 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) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt index d196e18fa..f9ed9230f 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -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 + ) } } diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt index 396df397e..067690afb 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/service/CheckSourceService.kt b/app/src/main/java/io/legado/app/service/CheckSourceService.kt index e6cbb5966..09900d0d0 100644 --- a/app/src/main/java/io/legado/app/service/CheckSourceService.kt +++ b/app/src/main/java/io/legado/app/service/CheckSourceService.kt @@ -24,6 +24,21 @@ class CheckSourceService : BaseService() { private val allIds = ArrayList() private val checkedIds = ArrayList() 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(this, "activity") + ) + .addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.cancel), + IntentHelp.servicePendingIntent(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(this, "activity") - ) - .addAction( - R.drawable.ic_stop_black_24dp, - getString(R.string.cancel), - IntentHelp.servicePendingIntent(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()) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/DownloadService.kt b/app/src/main/java/io/legado/app/service/DownloadService.kt new file mode 100644 index 000000000..53868a977 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/DownloadService.kt @@ -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() + private val completeDownloads = hashSetOf() + 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( + this, + IntentAction.play, + bundleOf("downloadId" to downloadId) + ) + ) + notificationBuilder.addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.cancel), + IntentHelp.servicePendingIntent( + this, + IntentAction.stop, + bundleOf("downloadId" to downloadId) + ) + ) + notificationBuilder.setDeleteIntent( + IntentHelp.servicePendingIntent( + 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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/help/Download.kt b/app/src/main/java/io/legado/app/service/help/Download.kt new file mode 100644 index 000000000..cddbf59b6 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/help/Download.kt @@ -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) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/help/ReadBook.kt b/app/src/main/java/io/legado/app/service/help/ReadBook.kt index e8485261c..2e96221f3 100644 --- a/app/src/main/java/io/legado/app/service/help/ReadBook.kt +++ b/app/src/main/java/io/legado/app/service/help/ReadBook.kt @@ -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, indexWithinChapter: Int, query: String): Array{ + // + // 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() } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 7840123db..7120e047a 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -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(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(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(R.layout.activity_boo intent.removeExtra("readAloud") ReadBook.readAloud() } + loadStates = true } /** @@ -543,6 +552,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo page_view.upContent(relativePosition, resetPageOffset) seek_read_page.progress = ReadBook.durPageIndex } + loadStates = false } /** @@ -667,6 +677,19 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo } } + /** + * 打开搜索界面 + */ + //todo: change request code + override fun openSearchList() { + ReadBook.book?.let { + startActivityForResult( + requestCodeSearchResult, + Pair("bookUrl", it.bookUrl) + ) + } + } + /** * 替换规则变化 */ @@ -747,11 +770,36 @@ class ReadBookActivity : VMBaseActivity(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() } + } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt index 6d069ff68..26423e701 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt @@ -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() diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt index 841a5b9a9..b8d71b515 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt @@ -324,7 +324,9 @@ class BgTextConfigDialog : BaseDialogFragment(), FileChooserDialog.CallBack { val fontName = FileUtils.getName(config.textFont) val fontPath = FileUtils.getPath(requireContext().externalFilesDir, "font", fontName) - FileUtils.getFile(configDir, fontName).copyTo(File(fontPath)) + if (!FileUtils.exist(fontPath)) { + FileUtils.getFile(configDir, fontName).copyTo(File(fontPath)) + } config.textFont = fontPath } if (config.bgType() == 2) { diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListActivity.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListActivity.kt new file mode 100644 index 000000000..ec1cbc0c7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListActivity.kt @@ -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(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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListAdapter.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListAdapter.kt new file mode 100644 index 000000000..d2c98b955 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListAdapter.kt @@ -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(context, R.layout.item_search_list) { + + val cacheFileNames = hashSetOf() + + override fun convert(holder: ItemViewHolder, item: SearchResult, payloads: MutableList) { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListFragment.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListFragment.kt new file mode 100644 index 000000000..38ac013df --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListFragment.kt @@ -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(R.layout.fragment_search_list), + SearchListAdapter.Callback, + SearchListViewModel.SearchListCallBack{ + override val viewModel: SearchListViewModel + get() = getViewModelOfActivity(SearchListViewModel::class.java) + + lateinit var adapter: SearchListAdapter + private lateinit var mLayoutManager: UpLinearLayoutManager + private var searchResultCounts = 0 + private var durChapterIndex = 0 + private var searchResultList: MutableList = mutableListOf() + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + viewModel.searchCallBack = this + val bbg = bottomBackground + val btc = requireContext().getPrimaryTextColor(ColorUtils.isColorLight(bbg)) + ll_search_base_info.setBackgroundColor(bbg) + tv_current_search_info.setTextColor(btc) + iv_search_content_top.setColorFilter(btc) + iv_search_content_bottom.setColorFilter(btc) + initRecyclerView() + initView() + initBook() + } + + private fun initRecyclerView() { + adapter = SearchListAdapter(requireContext(), this) + mLayoutManager = UpLinearLayoutManager(requireContext()) + recycler_view.layoutManager = mLayoutManager + recycler_view.addItemDecoration(VerticalDivider(requireContext())) + recycler_view.adapter = adapter + } + + private fun initView() { + iv_search_content_top.onClick { mLayoutManager.scrollToPositionWithOffset(0, 0) } + iv_search_content_bottom.onClick { + if (adapter.itemCount > 0) { + mLayoutManager.scrollToPositionWithOffset(adapter.itemCount - 1, 0) + } + } + } + + @SuppressLint("SetTextI18n") + private fun initBook() { + launch { + + tv_current_search_info.text = "搜索结果:$searchResultCounts" + viewModel.book?.let { + initCacheFileNames(it) + durChapterIndex = it.durChapterIndex + } + } + } + + private fun initCacheFileNames(book: Book) { + launch(IO) { + adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book)) + withContext(Main) { + adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true) + } + } + } + + override fun observeLiveBus() { + observeEvent(EventBus.SAVE_CONTENT) { chapter -> + viewModel.book?.bookUrl?.let { bookUrl -> + if (chapter.bookUrl == bookUrl) { + adapter.cacheFileNames.add(BookHelp.formatChapterName(chapter)) + adapter.notifyItemChanged(chapter.index, true) + } + } + } + } + + @SuppressLint("SetTextI18n") + override fun startContentSearch(newText: String) { + // 按章节搜索内容 + if (!newText.isBlank()) { + adapter.clearItems() + searchResultList.clear() + searchResultCounts = 0 + viewModel.lastQuery = newText + var searchResults = listOf() + launch(Main){ + App.db.bookChapterDao().getChapterList(viewModel.bookUrl).map{ chapter -> + val job = async(IO){ + if (isLocalBook || adapter.cacheFileNames.contains(BookHelp.formatChapterName(chapter))) { + searchResults = searchChapter(newText, chapter) + } + } + job.await() + if (searchResults.isNotEmpty()){ + searchResultList.addAll(searchResults) + tv_current_search_info.text = "搜索结果:$searchResultCounts" + adapter.addItems(searchResults) + searchResults = listOf() + } + } + } + } + } + + private suspend fun searchChapter(query: String, chapter: BookChapter?): List { + val searchResults: MutableList = mutableListOf() + var positions : List = listOf() + var replaceContents: List? = null + var totalContents = "" + if (chapter != null){ + viewModel.book?.let { bookSource -> + val bookContent = BookHelp.getContent(bookSource, chapter) + if (bookContent != null){ + //搜索替换后的正文 + val job = async(IO) { + chapter.title = when (AppConfig.chineseConverterType) { + 1 -> HanLP.convertToSimplifiedChinese(chapter.title) + 2 -> HanLP.convertToTraditionalChinese(chapter.title) + else -> chapter.title + } + replaceContents = BookHelp.disposeContent( + chapter.title, + bookSource.name, + bookSource.bookUrl, + bookContent, + bookSource.useReplaceRule + ) + } + job.await() + while (replaceContents == null){ + delay(100L) + } + totalContents = replaceContents!!.joinToString("") + positions = searchPosition(totalContents, query) + var count = 1 + positions.map{ + val construct = constructText(totalContents, it, query) + val result = SearchResult(index = searchResultCounts, + indexWithinChapter = count, + text = construct[1] as String, + chapterTitle = chapter.title, + query = query, + chapterIndex = chapter.index, + newPosition = construct[0] as Int, + contentPosition = it + ) + count += 1 + searchResultCounts += 1 + searchResults.add(result) + } + } + } + } + return searchResults + } + + private fun searchPosition(content: String, pattern: String): List { + val position : MutableList = mutableListOf() + var index = content.indexOf(pattern) + while(index >= 0){ + position.add(index) + index = content.indexOf(pattern, index + 1); + } + return position + } + + private fun constructText(content: String, position: Int, query: String): Array{ + // 构建关键词周边文字,在搜索结果里显示 + // todo: 判断段落,只在关键词所在段落内分割 + // todo: 利用标点符号分割完整的句 + // todo: length和设置结合,自由调整周边文字长度 + val length = 20 + var po1 = position - length + var po2 = position + query.length + length + if (po1 <0) { + po1 = 0 + } + if (po2 > content.length){ + po2 = content.length + } + val newPosition = position - po1 + val newText = content.substring(po1, po2) + return arrayOf(newPosition, newText) + } + + val isLocalBook: Boolean + get() = viewModel.book?.isLocalBook() == true + + override fun openSearchResult(searchResult: SearchResult) { + + val searchData = Intent() + searchData.putExtra("index", searchResult.chapterIndex) + searchData.putExtra("contentPosition", searchResult.contentPosition) + searchData.putExtra("query", searchResult.query) + searchData.putExtra("indexWithinChapter", searchResult.indexWithinChapter) + activity?.setResult(RESULT_OK, searchData) + activity?.finish() + + + } + + override fun durChapterIndex(): Int { + return durChapterIndex + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListViewModel.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListViewModel.kt new file mode 100644 index 000000000..060d74067 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchListViewModel.kt @@ -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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt new file mode 100644 index 000000000..3a629839b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt @@ -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) + + "($chapterTitle)" + } + + 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 "$sub1" + + "$center" + + "$sub2" + } + + fun parseText(targetText: String): Spanned { + return HtmlCompat.fromHtml(targetText, HtmlCompat.FROM_HTML_MODE_LEGACY) + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt index 59422c186..07f959719 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt @@ -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(R.layout.activity else -> data.reversed() } } - recycler_view.scrollToPosition(0) val diffResult = DiffUtil .calculateDiff(DiffCallBack(ArrayList(adapter.getItems()), sourceList)) adapter.setItems(sourceList, diffResult) diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt index 3e48c3ebf..c66cad69f 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt @@ -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(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) } } } diff --git a/app/src/main/res/layout/activity_search_list.xml b/app/src/main/res/layout/activity_search_list.xml new file mode 100644 index 000000000..159f77324 --- /dev/null +++ b/app/src/main/res/layout/activity_search_list.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_list.xml b/app/src/main/res/layout/fragment_search_list.xml new file mode 100644 index 000000000..79fefe11f --- /dev/null +++ b/app/src/main/res/layout/fragment_search_list.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_list.xml b/app/src/main/res/layout/item_search_list.xml new file mode 100644 index 000000000..2dcaf1436 --- /dev/null +++ b/app/src/main/res/layout/item_search_list.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_read_menu.xml b/app/src/main/res/layout/view_read_menu.xml index 1b9bc2a98..b63fba1ff 100644 --- a/app/src/main/res/layout/view_read_menu.xml +++ b/app/src/main/res/layout/view_read_menu.xml @@ -260,6 +260,45 @@ android:textSize="12sp" /> + + + + + + + + + + 切換默認主題 分享選中書源 時間排序 + 搜索 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c14a9a485..006b4ff45 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -761,6 +761,7 @@ 切換默認主題 分享選中書源 時間排序 + 搜索 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 036bcd6f9..78487a2be 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -761,5 +761,6 @@ 使用保存主题,导入,分享主题 切换默认主题 时间排序 + 搜索 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 92888ae19..893230b30 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -763,5 +763,6 @@ Save, Import, Share theme Share selected sources Sort by update time + Search \ No newline at end of file