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 59c88b2ad..ddfe64454 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 @@ -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 查找章节标题。 */ diff --git a/app/src/main/java/io/legado/app/help/book/BookHelp.kt b/app/src/main/java/io/legado/app/help/book/BookHelp.kt index 92d2ed0a5..2bfbb9b20 100644 --- a/app/src/main/java/io/legado/app/help/book/BookHelp.kt +++ b/app/src/main/java/io/legado/app/help/book/BookHelp.kt @@ -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 { 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, "") } -} +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/ContentEditDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/ContentEditDialog.kt index 6e8a52f77..66817fe36 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ContentEditDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ContentEditDialog.kt @@ -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) } } diff --git a/app/src/main/res/layout/dialog_content_edit.xml b/app/src/main/res/layout/dialog_content_edit.xml index 922430de0..6900cc49b 100644 --- a/app/src/main/res/layout/dialog_content_edit.xml +++ b/app/src/main/res/layout/dialog_content_edit.xml @@ -10,21 +10,32 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="?attr/actionBarStyle" - app:title="edit" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + app:title="edit" /> + + + app:layout_constraintTop_toBottomOf="@id/cb_save_to_source"> + + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@+id/tool_bar" /> \ No newline at end of file