[优化] 阅读本地TXT文本时,编辑可选择覆盖源文件
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user