[优化] 阅读本地TXT文本时,编辑可选择覆盖源文件
This commit is contained in:
4 files changed
+97
-47
No files matched your search
@@ -47,6 +47,9 @@ interface BookChapterDao {
|
|||||||
@Query("update chapters set wordCount = :wordCount where bookUrl = :bookUrl and url = :url")
|
@Query("update chapters set wordCount = :wordCount where bookUrl = :bookUrl and url = :url")
|
||||||
fun upWordCount(bookUrl: String, url: String, wordCount: String)
|
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 查找章节标题。
|
* 根据书籍的唯一标识 bookUrl 和章节索引 index 查找章节标题。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.legado.app.help.book
|
|||||||
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.system.Os
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.script.rhino.runScriptWithContext
|
import com.script.rhino.runScriptWithContext
|
||||||
import io.legado.app.constant.AppLog
|
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.help.config.AppConfig
|
||||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||||
import io.legado.app.model.localBook.LocalBook
|
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.ArchiveUtils
|
||||||
import io.legado.app.utils.FileUtils
|
import io.legado.app.utils.FileUtils
|
||||||
import io.legado.app.utils.ImageUtils
|
import io.legado.app.utils.ImageUtils
|
||||||
@@ -176,10 +178,19 @@ object BookHelp {
|
|||||||
fun saveText(
|
fun saveText(
|
||||||
book: Book,
|
book: Book,
|
||||||
bookChapter: BookChapter,
|
bookChapter: BookChapter,
|
||||||
content: String
|
content: String,
|
||||||
|
saveToSource: Boolean = false
|
||||||
) {
|
) {
|
||||||
if (content.isEmpty()) return
|
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(
|
FileUtils.createFileIfNotExist(
|
||||||
downloadDir,
|
downloadDir,
|
||||||
cacheFolderName,
|
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> {
|
fun flowImages(bookChapter: BookChapter, content: String): Flow<String> {
|
||||||
return flow {
|
return flow {
|
||||||
val matcher = AppPattern.imgPattern.matcher(content)
|
val matcher = AppPattern.imgPattern.matcher(content)
|
||||||
@@ -597,7 +662,7 @@ object BookHelp {
|
|||||||
|
|
||||||
private val regexC by lazy {
|
private val regexC by lazy {
|
||||||
//前后附加内容,整个章节名都在括号中时只剔除首尾括号,避免将章节名替换为空字串
|
//前后附加内容,整个章节名都在括号中时只剔除首尾括号,避免将章节名替换为空字串
|
||||||
return@lazy "(?!^)(?:[〖【《〔\\[{(][^〖【《〔\\[{()〕》】〗\\]}]+)?[)〕》】〗\\]}]$|^[〖【《〔\\[{(](?:[^〖【《〔\\[{()〕》】〗\\]}]+[〕》】〗\\]})])?(?!$)".toRegex()
|
return@lazy "(?!^)(?:[〖【《〔\\[{(][^〖【《〔\\[{()〕》》】〗\\]}]+)?[)〕》》】〗\\]}]$|^[〖【《〔\\[{(](?:[^〖【《〔\\[{()〕》》】〗\\]}]+[〕》》】〗\\]})])?(?!$)".toRegex()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPureChapterName(chapterName: String?): String {
|
private fun getPureChapterName(chapterName: String?): String {
|
||||||
@@ -608,4 +673,4 @@ object BookHelp {
|
|||||||
.replace(regexOther, "")
|
.replace(regexOther, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,6 @@ package io.legado.app.ui.book.read
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Spannable
|
|
||||||
import android.text.Spanned
|
|
||||||
import android.text.style.BackgroundColorSpan
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.MutableLiveData
|
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.BookHelp
|
||||||
import io.legado.app.help.book.ContentProcessor
|
import io.legado.app.help.book.ContentProcessor
|
||||||
import io.legado.app.help.book.isLocal
|
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.help.coroutine.Coroutine
|
||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
import io.legado.app.model.ReadBook
|
import io.legado.app.model.ReadBook
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.model.webBook.WebBook
|
||||||
import io.legado.app.utils.gone
|
import io.legado.app.utils.gone
|
||||||
import io.legado.app.utils.sendToClip
|
import io.legado.app.utils.sendToClip
|
||||||
import io.legado.app.utils.themeColor
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
import io.legado.app.utils.visible
|
import io.legado.app.utils.visible
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
@@ -47,12 +44,16 @@ class ContentEditDialog : BaseBottomSheetDialogFragment(R.layout.dialog_content_
|
|||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
binding.toolBar.title = ReadBook.curTextChapter?.title
|
binding.toolBar.title = ReadBook.curTextChapter?.title
|
||||||
|
val book = ReadBook.book
|
||||||
|
if (book?.isLocalTxt == true) {
|
||||||
|
binding.cbSaveToSource.visible()
|
||||||
|
}
|
||||||
initMenu()
|
initMenu()
|
||||||
binding.toolBar.setOnClickListener {
|
binding.toolBar.setOnClickListener {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val book = ReadBook.book ?: return@launch
|
val book1 = ReadBook.book ?: return@launch
|
||||||
val chapter = withContext(IO) {
|
val chapter = withContext(IO) {
|
||||||
appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex)
|
appDb.bookChapterDao.getChapter(book1.bookUrl, ReadBook.durChapterIndex)
|
||||||
} ?: return@launch
|
} ?: return@launch
|
||||||
editTitle(chapter)
|
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) {
|
override fun onCancel(dialog: DialogInterface) {
|
||||||
super.onCancel(dialog)
|
super.onCancel(dialog)
|
||||||
save()
|
save()
|
||||||
@@ -157,12 +127,13 @@ class ContentEditDialog : BaseBottomSheetDialogFragment(R.layout.dialog_content_
|
|||||||
|
|
||||||
private fun save() {
|
private fun save() {
|
||||||
val content = binding.contentView.text?.toString() ?: return
|
val content = binding.contentView.text?.toString() ?: return
|
||||||
|
val saveToSource = binding.cbSaveToSource.isChecked
|
||||||
Coroutine.async {
|
Coroutine.async {
|
||||||
val book = ReadBook.book ?: return@async
|
val book = ReadBook.book ?: return@async
|
||||||
val chapter = appDb.bookChapterDao
|
val chapter = appDb.bookChapterDao
|
||||||
.getChapter(book.bookUrl, ReadBook.durChapterIndex)
|
.getChapter(book.bookUrl, ReadBook.durChapterIndex)
|
||||||
?: return@async
|
?: return@async
|
||||||
BookHelp.saveText(book, chapter, content)
|
BookHelp.saveText(book, chapter, content, saveToSource)
|
||||||
ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false)
|
ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,21 +10,32 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:theme="?attr/actionBarStyle"
|
android:theme="?attr/actionBarStyle"
|
||||||
app:title="edit"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:title="edit" />
|
||||||
|
|
||||||
|
<com.google.android.material.materialswitch.MaterialSwitch
|
||||||
|
android:id="@+id/cb_save_to_source"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:text="保存到源文件,保存后重置功能将无效"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tool_bar" />
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.core.widget.NestedScrollView
|
||||||
android:id="@+id/scrollView"
|
android:id="@+id/scrollView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingHorizontal="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/tool_bar">
|
app:layout_constraintTop_toBottomOf="@id/cb_save_to_source">
|
||||||
|
|
||||||
<io.legado.app.ui.widget.text.TextInputLayout
|
<io.legado.app.ui.widget.text.TextInputLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="80dp"
|
android:layout_marginBottom="80dp"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/tool_bar">
|
app:layout_constraintTop_toBottomOf="@+id/tool_bar">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/content_view"
|
android:id="@+id/content_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -39,9 +50,9 @@
|
|||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
android:layout_height="60dp"
|
android:layout_height="60dp"
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/tool_bar"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent" />
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tool_bar" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
Reference in new issue
Block a user