This commit is contained in:
Horis 2023-08-23 12:31:43 +08:00
parent 4e044df3b2
commit 788a5b5d08
12 changed files with 231 additions and 31 deletions

View File

@ -177,6 +177,20 @@ object ReadBook : CoroutineScope by MainScope() {
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 {
if (durChapterIndex < chapterSize - 1) {
durChapterPos = 0

View File

@ -86,6 +86,9 @@ abstract class BaseReadAloudService : BaseService(),
BitmapFactory.decodeResource(appCtx.resources, R.drawable.icon_read_book)
var pageChanged = false
private var ttsProgress = 0
private var toLast = false
var paragraphStartPos = 0
private var readAloudByPage = false
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@ -164,19 +167,40 @@ abstract class BaseReadAloudService : BaseService(),
}
private fun newReadAloud(play: Boolean, pageIndex: Int, startPos: Int) {
lifecycleScope.launch(IO) {
execute(executeContext = IO) {
this@BaseReadAloudService.pageIndex = pageIndex
textChapter = ReadBook.curTextChapter
val textChapter = textChapter ?: return@launch
nowSpeak = 0
val textChapter = textChapter ?: return@execute
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")
.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) {
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) {
playStop()
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()
} else {
toLast = true
ReadBook.moveToPrevChapter(true)
}
}
@ -235,8 +274,10 @@ abstract class BaseReadAloudService : BaseService(),
private fun nextP() {
if (nowSpeak < contentList.size - 1) {
playStop()
readAloudNumber += contentList[nowSpeak].length.plus(1)
readAloudNumber += contentList[nowSpeak].length.plus(1) - paragraphStartPos
paragraphStartPos = 0
nowSpeak++
upTtsProgress(readAloudNumber + 1)
play()
} else {
nextChapter()

View File

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

View File

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

View File

@ -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()
}
}
}

View File

@ -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() {
@ -144,5 +152,6 @@ class RemoteBookAdapter(context: Context, val callBack: CallBack) :
fun openDir(remoteBook: RemoteBook)
fun upCountView()
fun startRead(remoteBook: RemoteBook)
fun addToBookShelfAgain(remoteBook: RemoteBook)
}
}
}

View File

@ -3,6 +3,7 @@ package io.legado.app.ui.book.manage
import android.app.Application
import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog
import io.legado.app.constant.BookType
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
@ -52,22 +53,21 @@ class BookshelfManageViewModel(application: Application) : BaseViewModel(applica
batchChangeSourceCoroutine?.cancel()
batchChangeSourceCoroutine = execute {
books.forEachIndexed { index, book ->
batchChangeSourceProcessLiveData.postValue("${index + 1}/${books.size}")
batchChangeSourceProcessLiveData.postValue("${index + 1} / ${books.size}")
if (book.isLocal) 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 {
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 {

View File

@ -40,6 +40,38 @@ data class TextChapter(
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 页数
* @return 是否是最后一页
@ -73,6 +105,18 @@ data class TextChapter(
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()
}
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 根据索引位置获取所在页
*/

View File

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

View File

@ -36,6 +36,23 @@ data class TextPage(
val searchResult = hashSetOf<TextColumn>()
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) {
textLines.add(line)
}

View File

@ -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()
}
}

View File

@ -417,10 +417,23 @@ object ChapterProvider {
}
}
}
val sbLength = stringBuilder.length
stringBuilder.append(words)
if (textLine.isParagraphEnd) {
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)
textLine.upTopBottom(durY, textPaint)
durY += textPaint.textHeight * lineSpacingExtra