[优化] 阅读本地TXT文本时,编辑可选择覆盖源文件

This commit is contained in:
HapeLee
2026-03-05 01:23:47 +08:00
parent 16b8164943
commit c528ed59a5
4 changed files with 97 additions and 47 deletions
@@ -47,6 +47,9 @@ interface BookChapterDao {
@Query("update chapters set wordCount = :wordCount where bookUrl = :bookUrl and url = :url")
fun upWordCount(bookUrl: String, url: String, wordCount: String)
@Query("update chapters set start = start + :diff, end = end + :diff where bookUrl = :bookUrl and `index` > :index")
fun updateOffsets(bookUrl: String, index: Int, diff: Long)
/**
* 根据书籍的唯一标识 bookUrl 和章节索引 index 查找章节标题。
*/
@@ -2,6 +2,7 @@ package io.legado.app.help.book
import android.graphics.BitmapFactory
import android.os.ParcelFileDescriptor
import android.system.Os
import androidx.documentfile.provider.DocumentFile
import com.script.rhino.runScriptWithContext
import io.legado.app.constant.AppLog
@@ -14,6 +15,7 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.help.config.AppConfig
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.localBook.TextFile
import io.legado.app.utils.ArchiveUtils
import io.legado.app.utils.FileUtils
import io.legado.app.utils.ImageUtils
@@ -176,10 +178,19 @@ object BookHelp {
fun saveText(
book: Book,
bookChapter: BookChapter,
content: String
content: String,
saveToSource: Boolean = false
) {
if (content.isEmpty()) return
//保存文本
if (book.isLocalTxt && saveToSource) {
try {
saveToLocalTxt(book, bookChapter, content)
TextFile.clear()
} catch (e: Exception) {
AppLog.put("修改本地TXT失败: ${e.localizedMessage}", e)
}
}
// 保存阅读缓存文本(.nb)
FileUtils.createFileIfNotExist(
downloadDir,
cacheFolderName,
@@ -193,6 +204,60 @@ object BookHelp {
}
}
private fun saveToLocalTxt(book: Book, bookChapter: BookChapter, content: String) {
val start = bookChapter.start ?: return
val end = bookChapter.end ?: return
val uri = book.getLocalUri()
val charset = book.fileCharset()
val oldTitle = bookChapter.title
val newContent = oldTitle + "\n" + content
val newBytes = newContent.toByteArray(charset)
val oldLength = end - start
val diff = newBytes.size - oldLength
val pfd = if (uri.isContentScheme()) {
appCtx.contentResolver.openFileDescriptor(uri, "rw")
} else {
ParcelFileDescriptor.open(File(uri.path!!), ParcelFileDescriptor.MODE_READ_WRITE)
}
pfd?.use {
val fd = it.fileDescriptor
try {
val totalLength = Os.fstat(fd).st_size
if (diff != 0L) {
val remaining = totalLength - end
if (remaining > 0) {
val buffer = ByteArray(1024 * 1024)
var pos = totalLength
while (pos > end) {
val readSize =
if (pos - end > buffer.size) buffer.size else (pos - end).toInt()
Os.pread(fd, buffer, 0, readSize, pos - readSize)
Os.pwrite(fd, buffer, 0, readSize, pos - readSize + diff)
pos -= readSize
}
}
if (diff < 0) {
Os.ftruncate(fd, totalLength + diff)
}
}
Os.pwrite(fd, newBytes, 0, newBytes.size, start)
// 更新数据库中的偏移量
if (diff != 0L) {
appDb.bookChapterDao.updateOffsets(book.bookUrl, bookChapter.index, diff)
}
bookChapter.end = start + newBytes.size
appDb.bookChapterDao.update(bookChapter)
} catch (e: Exception) {
throw e
}
}
}
fun flowImages(bookChapter: BookChapter, content: String): Flow<String> {
return flow {
val matcher = AppPattern.imgPattern.matcher(content)
@@ -597,7 +662,7 @@ object BookHelp {
private val regexC by lazy {
//前后附加内容,整个章节名都在括号中时只剔除首尾括号,避免将章节名替换为空字串
return@lazy "(?!^)(?:[〖【《〔\\[{(][^〖【《〔\\[{()〕》】〗\\]}]+)?[)〕》】〗\\]}]$|^[〖【《〔\\[{(](?:[^〖【《〔\\[{()〕》】〗\\]}]+[〕》】〗\\]})])?(?!$)".toRegex()
return@lazy "(?!^)(?:[〖【《〔\\[{(][^〖【《〔\\[{()〕》】〗\\]}]+)?[)〕》】〗\\]}]$|^[〖【《〔\\[{(](?:[^〖【《〔\\[{()〕》】〗\\]}]+[〕》】〗\\]})])?(?!$)".toRegex()
}
private fun getPureChapterName(chapterName: String?): String {
@@ -608,4 +673,4 @@ object BookHelp {
.replace(regexOther, "")
}
}
}
@@ -3,9 +3,6 @@ package io.legado.app.ui.book.read
import android.app.Application
import android.content.DialogInterface
import android.os.Bundle
import android.text.Spannable
import android.text.Spanned
import android.text.style.BackgroundColorSpan
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData
@@ -20,13 +17,13 @@ import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.book.isLocal
import io.legado.app.help.book.isLocalTxt
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.dialogs.alert
import io.legado.app.model.ReadBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.gone
import io.legado.app.utils.sendToClip
import io.legado.app.utils.themeColor
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible
import kotlinx.coroutines.Dispatchers.IO
@@ -47,12 +44,16 @@ class ContentEditDialog : BaseBottomSheetDialogFragment(R.layout.dialog_content_
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.title = ReadBook.curTextChapter?.title
val book = ReadBook.book
if (book?.isLocalTxt == true) {
binding.cbSaveToSource.visible()
}
initMenu()
binding.toolBar.setOnClickListener {
lifecycleScope.launch {
val book = ReadBook.book ?: return@launch
val book1 = ReadBook.book ?: return@launch
val chapter = withContext(IO) {
appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex)
appDb.bookChapterDao.getChapter(book1.bookUrl, ReadBook.durChapterIndex)
} ?: return@launch
editTitle(chapter)
}
@@ -119,37 +120,6 @@ class ContentEditDialog : BaseBottomSheetDialogFragment(R.layout.dialog_content_
}
}
private fun highlightSelectedTextTwice() {
val editText = binding.contentView
val content = editText.text ?: return
val start = arguments?.getInt("start_position", -1) ?: -1
val selectedText = arguments?.getString("selected_text") ?: ""
if (start == -1 || selectedText.isEmpty()) return
val end = (start + selectedText.length).coerceAtMost(content.length)
val spannable = editText.text as Spannable
val span =
BackgroundColorSpan(requireContext().themeColor(com.google.android.material.R.attr.colorSecondaryContainer))
fun flash(times: Int) {
if (times <= 0) return
spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
editText.postDelayed({
spannable.removeSpan(span)
editText.postDelayed({
flash(times - 1)
}, 200)
}, 300)
}
flash(3)
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
save()
@@ -157,12 +127,13 @@ class ContentEditDialog : BaseBottomSheetDialogFragment(R.layout.dialog_content_
private fun save() {
val content = binding.contentView.text?.toString() ?: return
val saveToSource = binding.cbSaveToSource.isChecked
Coroutine.async {
val book = ReadBook.book ?: return@async
val chapter = appDb.bookChapterDao
.getChapter(book.bookUrl, ReadBook.durChapterIndex)
?: return@async
BookHelp.saveText(book, chapter, content)
BookHelp.saveText(book, chapter, content, saveToSource)
ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false)
}
}