Horis 3 лет назад
Родитель
Сommit
367646d7b1

+ 14 - 0
app/src/main/java/io/legado/app/model/ReadBook.kt

@@ -177,6 +177,20 @@ object ReadBook : CoroutineScope by MainScope() {
         return hasNextPage
         return hasNextPage
     }
     }
 
 
+    fun moveToPrevPage(): Boolean {
+        var hasPrevPage = false
+        curTextChapter?.let {
+            val prevPagePos = it.getPrevPageLength(durChapterPos)
+            if (prevPagePos >= 0) {
+                hasPrevPage = true
+                durChapterPos = prevPagePos
+                callBack?.upContent()
+                saveRead()
+            }
+        }
+        return hasPrevPage
+    }
+
     fun moveToNextChapter(upContent: Boolean): Boolean {
     fun moveToNextChapter(upContent: Boolean): Boolean {
         if (durChapterIndex < chapterSize - 1) {
         if (durChapterIndex < chapterSize - 1) {
             durChapterPos = 0
             durChapterPos = 0

+ 48 - 7
app/src/main/java/io/legado/app/service/BaseReadAloudService.kt

@@ -86,6 +86,9 @@ abstract class BaseReadAloudService : BaseService(),
         BitmapFactory.decodeResource(appCtx.resources, R.drawable.icon_read_book)
         BitmapFactory.decodeResource(appCtx.resources, R.drawable.icon_read_book)
     var pageChanged = false
     var pageChanged = false
     private var ttsProgress = 0
     private var ttsProgress = 0
+    private var toLast = false
+    var paragraphStartPos = 0
+    private var readAloudByPage = false
 
 
     private val broadcastReceiver = object : BroadcastReceiver() {
     private val broadcastReceiver = object : BroadcastReceiver() {
         override fun onReceive(context: Context, intent: Intent) {
         override fun onReceive(context: Context, intent: Intent) {
@@ -164,19 +167,40 @@ abstract class BaseReadAloudService : BaseService(),
     }
     }
 
 
     private fun newReadAloud(play: Boolean, pageIndex: Int, startPos: Int) {
     private fun newReadAloud(play: Boolean, pageIndex: Int, startPos: Int) {
-        lifecycleScope.launch(IO) {
+        execute(executeContext = IO) {
             this@BaseReadAloudService.pageIndex = pageIndex
             this@BaseReadAloudService.pageIndex = pageIndex
             textChapter = ReadBook.curTextChapter
             textChapter = ReadBook.curTextChapter
-            val textChapter = textChapter ?: return@launch
-            nowSpeak = 0
+            val textChapter = textChapter ?: return@execute
             readAloudNumber = textChapter.getReadLength(pageIndex) + startPos
             readAloudNumber = textChapter.getReadLength(pageIndex) + startPos
-            val readAloudByPage = getPrefBoolean(PreferKey.readAloudByPage)
-            contentList = textChapter.getNeedReadAloud(pageIndex, readAloudByPage, startPos)
+            readAloudByPage = getPrefBoolean(PreferKey.readAloudByPage)
+            contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0)
                 .split("\n")
                 .split("\n")
                 .filter { it.isNotEmpty() }
                 .filter { it.isNotEmpty() }
+            var pos = startPos
+            val page = textChapter.getPage(pageIndex)!!
+            if (pos > 0) {
+                for (paragraph in page.paragraphs) {
+                    val tmp = pos - paragraph.length - 1
+                    if (tmp < 0) break
+                    pos = tmp
+                }
+            }
+            nowSpeak = textChapter.getParagraphNum(readAloudNumber + 1, readAloudByPage) - 1
+            if (!readAloudByPage && pos == 0) {
+                pos = page.lines.first().chapterPosition -
+                        textChapter.paragraphs[nowSpeak].chapterPosition
+            }
+            paragraphStartPos = pos
+            if (toLast) {
+                toLast = false
+                readAloudNumber = textChapter.getLastParagraphPosition()
+                nowSpeak = contentList.lastIndex
+            }
             launch(Main) {
             launch(Main) {
                 if (play) play() else pageChanged = true
                 if (play) play() else pageChanged = true
             }
             }
+        }.onError {
+            AppLog.put("启动朗读出错\n${it.localizedMessage}", it, true)
         }
         }
     }
     }
 
 
@@ -225,9 +249,24 @@ abstract class BaseReadAloudService : BaseService(),
         if (nowSpeak > 0) {
         if (nowSpeak > 0) {
             playStop()
             playStop()
             nowSpeak--
             nowSpeak--
-            readAloudNumber -= contentList[nowSpeak].length.minus(1)
+            readAloudNumber -= contentList[nowSpeak].length + 1 + paragraphStartPos
+            paragraphStartPos = 0
+            textChapter?.let {
+                val paragraphs = if (readAloudByPage) {
+                    it.pageParagraphs
+                } else {
+                    it.paragraphs
+                }
+                if (!paragraphs[nowSpeak].isParagraphEnd) readAloudNumber++
+                if (readAloudNumber < it.getReadLength(pageIndex)) {
+                    pageIndex--
+                    ReadBook.moveToPrevPage()
+                }
+            }
+            upTtsProgress(readAloudNumber + 1)
             play()
             play()
         } else {
         } else {
+            toLast = true
             ReadBook.moveToPrevChapter(true)
             ReadBook.moveToPrevChapter(true)
         }
         }
     }
     }
@@ -235,8 +274,10 @@ abstract class BaseReadAloudService : BaseService(),
     private fun nextP() {
     private fun nextP() {
         if (nowSpeak < contentList.size - 1) {
         if (nowSpeak < contentList.size - 1) {
             playStop()
             playStop()
-            readAloudNumber += contentList[nowSpeak].length.plus(1)
+            readAloudNumber += contentList[nowSpeak].length.plus(1) - paragraphStartPos
+            paragraphStartPos = 0
             nowSpeak++
             nowSpeak++
+            upTtsProgress(readAloudNumber + 1)
             play()
             play()
         } else {
         } else {
             nextChapter()
             nextChapter()

+ 10 - 6
app/src/main/java/io/legado/app/service/HttpReadAloudService.kt

@@ -11,7 +11,6 @@ import com.script.ScriptException
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern
 import io.legado.app.constant.AppPattern
-import io.legado.app.constant.EventBus
 import io.legado.app.data.entities.HttpTTS
 import io.legado.app.data.entities.HttpTTS
 import io.legado.app.exception.ConcurrentException
 import io.legado.app.exception.ConcurrentException
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
@@ -22,7 +21,6 @@ import io.legado.app.model.ReadBook
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.MD5Utils
-import io.legado.app.utils.postEvent
 import io.legado.app.utils.printOnDebug
 import io.legado.app.utils.printOnDebug
 import io.legado.app.utils.servicePendingIntent
 import io.legado.app.utils.servicePendingIntent
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
@@ -90,10 +88,12 @@ class HttpReadAloudService : BaseReadAloudService(),
 
 
     override fun playStop() {
     override fun playStop() {
         exoPlayer.stop()
         exoPlayer.stop()
+        playIndexJob?.cancel()
     }
     }
 
 
     private fun playNext() {
     private fun playNext() {
-        readAloudNumber += contentList[nowSpeak].length + 1
+        readAloudNumber += contentList[nowSpeak].length + 1 - paragraphStartPos
+        paragraphStartPos = 0
         if (nowSpeak < contentList.lastIndex) {
         if (nowSpeak < contentList.lastIndex) {
             nowSpeak++
             nowSpeak++
         } else {
         } else {
@@ -111,10 +111,14 @@ class HttpReadAloudService : BaseReadAloudService(),
                 contentList.forEachIndexed { index, content ->
                 contentList.forEachIndexed { index, content ->
                     ensureActive()
                     ensureActive()
                     if (index < nowSpeak) return@forEachIndexed
                     if (index < nowSpeak) return@forEachIndexed
-                    val fileName = md5SpeakFileName(content)
-                    val speakText = content.replace(AppPattern.notReadAloudRegex, "")
+                    var text = content
+                    if (paragraphStartPos > 0 && index == nowSpeak) {
+                        text = text.substring(paragraphStartPos)
+                    }
+                    val fileName = md5SpeakFileName(text)
+                    val speakText = text.replace(AppPattern.notReadAloudRegex, "")
                     if (speakText.isEmpty()) {
                     if (speakText.isEmpty()) {
-                        AppLog.put("阅读段落内容为空,使用无声音频代替。\n朗读文本:$content")
+                        AppLog.put("阅读段落内容为空,使用无声音频代替。\n朗读文本:$text")
                         createSilentSound(fileName)
                         createSilentSound(fileName)
                     } else if (!hasSpeakFile(fileName)) {
                     } else if (!hasSpeakFile(fileName)) {
                         runCatching {
                         runCatching {

+ 6 - 3
app/src/main/java/io/legado/app/service/TTSReadAloudService.kt

@@ -7,7 +7,6 @@ import io.legado.app.R
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern
 import io.legado.app.constant.AppPattern
-import io.legado.app.constant.EventBus
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.MediaHelp
 import io.legado.app.help.MediaHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
@@ -100,7 +99,10 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
             val contentList = contentList
             val contentList = contentList
             for (i in nowSpeak until contentList.size) {
             for (i in nowSpeak until contentList.size) {
                 ensureActive()
                 ensureActive()
-                val text = contentList[i]
+                var text = contentList[i]
+                if (paragraphStartPos > 0 && i == nowSpeak) {
+                    text = text.substring(paragraphStartPos)
+                }
                 if (text.matches(AppPattern.notReadAloudRegex)) {
                 if (text.matches(AppPattern.notReadAloudRegex)) {
                     continue
                     continue
                 }
                 }
@@ -178,7 +180,8 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
         override fun onDone(s: String) {
         override fun onDone(s: String) {
             //跳过全标点段落
             //跳过全标点段落
             do {
             do {
-                readAloudNumber += contentList[nowSpeak].length + 1
+                readAloudNumber += contentList[nowSpeak].length + 1 - paragraphStartPos
+                paragraphStartPos = 0
                 nowSpeak++
                 nowSpeak++
                 if (nowSpeak >= contentList.size) {
                 if (nowSpeak >= contentList.size) {
                     nextChapter()
                     nextChapter()

+ 13 - 1
app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookActivity.kt

@@ -247,4 +247,16 @@ class RemoteBookActivity : BaseImportBookActivity<RemoteBookViewModel>(),
         }
         }
     }
     }
 
 
-}
+    override fun addToBookShelfAgain(remoteBook: RemoteBook) {
+        alert(getString(R.string.sure), "是否重新加入书架?") {
+            yesButton {
+                binding.refreshProgressBar.isAutoLoading = true
+                viewModel.addToBookshelf(hashSetOf(remoteBook)) {
+                    binding.refreshProgressBar.isAutoLoading = false
+                }
+            }
+            noButton()
+        }
+    }
+
+}

+ 10 - 1
app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookAdapter.kt

@@ -90,6 +90,14 @@ class RemoteBookAdapter(context: Context, val callBack: CallBack) :
                 }
                 }
             }
             }
         }
         }
+        holder.itemView.setOnLongClickListener {
+            getItem(holder.layoutPosition)?.let { remoteBook ->
+                if (remoteBook.isOnBookShelf) {
+                    callBack.addToBookShelfAgain(remoteBook)
+                }
+            }
+            true
+        }
     }
     }
 
 
     private fun upCheckableCount() {
     private fun upCheckableCount() {
@@ -144,5 +152,6 @@ class RemoteBookAdapter(context: Context, val callBack: CallBack) :
         fun openDir(remoteBook: RemoteBook)
         fun openDir(remoteBook: RemoteBook)
         fun upCountView()
         fun upCountView()
         fun startRead(remoteBook: RemoteBook)
         fun startRead(remoteBook: RemoteBook)
+        fun addToBookShelfAgain(remoteBook: RemoteBook)
     }
     }
-}
+}

+ 13 - 13
app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt

@@ -3,6 +3,7 @@ package io.legado.app.ui.book.manage
 import android.app.Application
 import android.app.Application
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.MutableLiveData
 import io.legado.app.base.BaseViewModel
 import io.legado.app.base.BaseViewModel
+import io.legado.app.constant.AppLog
 import io.legado.app.constant.BookType
 import io.legado.app.constant.BookType
 import io.legado.app.data.appDb
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
@@ -52,22 +53,21 @@ class BookshelfManageViewModel(application: Application) : BaseViewModel(applica
         batchChangeSourceCoroutine?.cancel()
         batchChangeSourceCoroutine?.cancel()
         batchChangeSourceCoroutine = execute {
         batchChangeSourceCoroutine = execute {
             books.forEachIndexed { index, book ->
             books.forEachIndexed { index, book ->
-                batchChangeSourceProcessLiveData.postValue("${index + 1}/${books.size}")
+                batchChangeSourceProcessLiveData.postValue("${index + 1} / ${books.size}")
                 if (book.isLocal) return@forEachIndexed
                 if (book.isLocal) return@forEachIndexed
                 if (book.origin == source.bookSourceUrl) return@forEachIndexed
                 if (book.origin == source.bookSourceUrl) return@forEachIndexed
-                WebBook.preciseSearchAwait(this, source, book.name, book.author)
+                val newBook = WebBook.preciseSearchAwait(this, source, book.name, book.author)
                     .onFailure {
                     .onFailure {
-                        context.toastOnUi("获取书籍出错\n${it.localizedMessage}")
-                    }.getOrNull()?.let { newBook ->
-                        WebBook.getChapterListAwait(source, newBook)
-                            .onFailure {
-                                context.toastOnUi("获取目录出错\n${it.localizedMessage}")
-                            }.getOrNull()?.let { toc ->
-                                book.migrateTo(newBook, toc)
-                                book.removeType(BookType.updateError)
-                                appDb.bookDao.insert(newBook)
-                                appDb.bookChapterDao.insert(*toc.toTypedArray())
-                            }
+                        AppLog.put("获取书籍出错\n${it.localizedMessage}", it, true)
+                    }.getOrNull() ?: return@forEachIndexed
+                WebBook.getChapterListAwait(source, newBook)
+                    .onFailure {
+                        AppLog.put("获取目录出错\n${it.localizedMessage}", it, true)
+                    }.getOrNull()?.let { toc ->
+                        book.migrateTo(newBook, toc)
+                        book.removeType(BookType.updateError)
+                        appDb.bookDao.insert(newBook)
+                        appDb.bookChapterDao.insert(*toc.toTypedArray())
                     }
                     }
             }
             }
         }.onStart {
         }.onStart {

+ 65 - 0
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt

@@ -40,6 +40,38 @@ data class TextChapter(
 
 
     val pageSize: Int get() = pages.size
     val pageSize: Int get() = pages.size
 
 
+    val paragraphs by lazy {
+        paragraphsInternal
+    }
+
+    val pageParagraphs by lazy {
+        pageParagraphsInternal
+    }
+
+    val paragraphsInternal: ArrayList<TextParagraph>
+        get() {
+            val paragraphs = arrayListOf<TextParagraph>()
+            pages.forEach {
+                it.lines.forEach loop@{ line ->
+                    if (line.paragraphNum <= 0) return@loop
+                    if (paragraphs.lastIndex < line.paragraphNum - 1) {
+                        paragraphs.add(TextParagraph(line.paragraphNum))
+                    }
+                    paragraphs[line.paragraphNum - 1].textLines.add(line)
+                }
+            }
+            return paragraphs
+        }
+
+    val pageParagraphsInternal: List<TextParagraph>
+        get() = pages.map {
+            it.paragraphs
+        }.flatten().also {
+            it.forEachIndexed { index, textParagraph ->
+                textParagraph.num = index + 1
+            }
+        }
+
     /**
     /**
      * @param index 页数
      * @param index 页数
      * @return 是否是最后一页
      * @return 是否是最后一页
@@ -73,6 +105,18 @@ data class TextChapter(
         return getReadLength(pageIndex + 1)
         return getReadLength(pageIndex + 1)
     }
     }
 
 
+    /**
+     * @param length 当前页面文字在章节中的位置
+     * @return 上一页位置,如果没有上一页返回-1
+     */
+    fun getPrevPageLength(length: Int): Int {
+        val pageIndex = getPageIndexByCharIndex(length)
+        if (pageIndex - 1 < 0) {
+            return -1
+        }
+        return getReadLength(pageIndex - 1)
+    }
+
     /**
     /**
      * 获取内容
      * 获取内容
      */
      */
@@ -116,6 +160,27 @@ data class TextChapter(
         return stringBuilder.substring(startPos).toString()
         return stringBuilder.substring(startPos).toString()
     }
     }
 
 
+    fun getParagraphNum(
+        position: Int,
+        pageSplit: Boolean,
+    ): Int {
+        val paragraphs = if (pageSplit) {
+            pageParagraphs
+        } else {
+            paragraphs
+        }
+        paragraphs.forEach { paragraph ->
+            if (position in paragraph.chapterIndices) {
+                return paragraph.num
+            }
+        }
+        return -1
+    }
+
+    fun getLastParagraphPosition(): Int {
+        return pageParagraphs.last().chapterPosition
+    }
+
     /**
     /**
      * @return 根据索引位置获取所在页
      * @return 根据索引位置获取所在页
      */
      */

+ 4 - 0
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt

@@ -18,6 +18,9 @@ data class TextLine(
     var lineBase: Float = 0f,
     var lineBase: Float = 0f,
     var lineBottom: Float = 0f,
     var lineBottom: Float = 0f,
     var indentWidth: Float = 0f,
     var indentWidth: Float = 0f,
+    var paragraphNum: Int = 0,
+    var chapterPosition: Int = 0,
+    var pagePosition: Int = 0,
     val isTitle: Boolean = false,
     val isTitle: Boolean = false,
     var isParagraphEnd: Boolean = false,
     var isParagraphEnd: Boolean = false,
     var isReadAloud: Boolean = false,
     var isReadAloud: Boolean = false,
@@ -28,6 +31,7 @@ data class TextLine(
     val charSize: Int get() = textColumns.size
     val charSize: Int get() = textColumns.size
     val lineStart: Float get() = textColumns.firstOrNull()?.start ?: 0f
     val lineStart: Float get() = textColumns.firstOrNull()?.start ?: 0f
     val lineEnd: Float get() = textColumns.lastOrNull()?.end ?: 0f
     val lineEnd: Float get() = textColumns.lastOrNull()?.end ?: 0f
+    val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize
 
 
     fun addColumn(column: BaseColumn) {
     fun addColumn(column: BaseColumn) {
         textColumns.add(column)
         textColumns.add(column)

+ 17 - 0
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt

@@ -36,6 +36,23 @@ data class TextPage(
     val searchResult = hashSetOf<TextColumn>()
     val searchResult = hashSetOf<TextColumn>()
     var isMsgPage: Boolean = false
     var isMsgPage: Boolean = false
 
 
+    val paragraphs by lazy {
+        paragraphsInternal
+    }
+
+    val paragraphsInternal: ArrayList<TextParagraph> get() {
+        val paragraphs = arrayListOf<TextParagraph>()
+        val lines = textLines.filter { it.paragraphNum > 0 }
+        val offset = lines.first().paragraphNum - 1
+        lines.forEach { line ->
+            if (paragraphs.lastIndex < line.paragraphNum - offset - 1) {
+                paragraphs.add(TextParagraph(0))
+            }
+            paragraphs[line.paragraphNum - offset - 1].textLines.add(line)
+        }
+        return paragraphs
+    }
+
     fun addLine(line: TextLine) {
     fun addLine(line: TextLine) {
         textLines.add(line)
         textLines.add(line)
     }
     }

+ 18 - 0
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextParagraph.kt

@@ -0,0 +1,18 @@
+package io.legado.app.ui.book.read.page.entities
+
+@Suppress("unused")
+data class TextParagraph(
+    var num: Int,
+    val textLines: ArrayList<TextLine> = arrayListOf(),
+) : Iterable<TextLine> {
+    val text: String get() = textLines.joinToString("") { it.text }
+    val length: Int get() = text.length
+    val chapterIndices: IntRange get() = first().chapterPosition..last().chapterPosition + last().charSize
+    val chapterPosition: Int get() = first().chapterPosition
+    val realNum: Int get() = first().paragraphNum
+    val isParagraphEnd: Boolean get() = last().isParagraphEnd
+
+    override fun iterator(): Iterator<TextLine> {
+        return textLines.iterator()
+    }
+}

+ 13 - 0
app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt

@@ -417,10 +417,23 @@ object ChapterProvider {
                     }
                     }
                 }
                 }
             }
             }
+            val sbLength = stringBuilder.length
             stringBuilder.append(words)
             stringBuilder.append(words)
             if (textLine.isParagraphEnd) {
             if (textLine.isParagraphEnd) {
                 stringBuilder.append("\n")
                 stringBuilder.append("\n")
             }
             }
+            val lastLine = textPages.last().lines.lastOrNull { it.paragraphNum > 0 }
+                ?: textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull { it.paragraphNum > 0 }
+            val paragraphNum = when {
+                lastLine == null -> 1
+                lastLine.isParagraphEnd -> lastLine.paragraphNum + 1
+                else -> lastLine.paragraphNum
+            }
+            textLine.paragraphNum = paragraphNum
+            textLine.chapterPosition = textPages.foldIndexed(sbLength) { index, acc, textPage ->
+                acc + if (index == textPages.lastIndex) 0 else textPage.text.length
+            }
+            textLine.pagePosition = sbLength
             textPages.last().addLine(textLine)
             textPages.last().addLine(textLine)
             textLine.upTopBottom(durY, textPaint)
             textLine.upTopBottom(durY, textPaint)
             durY += textPaint.textHeight * lineSpacingExtra
             durY += textPaint.textHeight * lineSpacingExtra