|
@@ -145,6 +145,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
var bookSource: BookSource? = null
|
|
var bookSource: BookSource? = null
|
|
|
var msg: String? = null
|
|
var msg: String? = null
|
|
|
private val readRecordRepository: ReadRecordRepository by inject()
|
|
private val readRecordRepository: ReadRecordRepository by inject()
|
|
|
|
|
+ private val readAloudSessionStore: ReadAloudSessionStore by inject()
|
|
|
private var lastReadLength: Long = 0
|
|
private var lastReadLength: Long = 0
|
|
|
private val loadingChapters = arrayListOf<Int>()
|
|
private val loadingChapters = arrayListOf<Int>()
|
|
|
private val readRecord = ReadRecord()
|
|
private val readRecord = ReadRecord()
|
|
@@ -165,6 +166,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
|
|
|
|
|
/* 跳转进度前进度记录 */
|
|
/* 跳转进度前进度记录 */
|
|
|
var lastBookProgress: BookProgress? = null
|
|
var lastBookProgress: BookProgress? = null
|
|
|
|
|
+ private var readingAnchorJumpCount = 0
|
|
|
|
|
|
|
|
/* web端阅读进度记录 */
|
|
/* web端阅读进度记录 */
|
|
|
var webBookProgress: BookProgress? = null
|
|
var webBookProgress: BookProgress? = null
|
|
@@ -223,6 +225,26 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
*/
|
|
*/
|
|
|
private fun publishSnapshot() {
|
|
private fun publishSnapshot() {
|
|
|
_snapshot.value = buildSnapshot()
|
|
_snapshot.value = buildSnapshot()
|
|
|
|
|
+ restoreReadAloudFollowIfBackOnPosition()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 页面回到朗读所在位置时自动恢复跟随:手动翻页脱离后,翻回朗读位置(同章同页)即视为重新跟随,
|
|
|
|
|
+ * 悬浮条随之消失;同时重绘朗读高亮(翻页时旧页高亮已被移除)。朗读服务自身驱动的页面移动不参与。
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun restoreReadAloudFollowIfBackOnPosition() {
|
|
|
|
|
+ if (!BaseReadAloudService.isRun || BaseReadAloudService.speechDrivingNavigation) return
|
|
|
|
|
+ val speakingChapterIndex = BaseReadAloudService.currentChapterIndex
|
|
|
|
|
+ if (speakingChapterIndex < 0 || speakingChapterIndex != durChapterIndex) return
|
|
|
|
|
+ val speakingPage = curTextChapter?.getPageIndexByCharIndex(
|
|
|
|
|
+ BaseReadAloudService.currentProgress.coerceAtLeast(0)
|
|
|
|
|
+ ) ?: return
|
|
|
|
|
+ if (speakingPage == durPageIndex) {
|
|
|
|
|
+ if (!readAloudSessionStore.state.value.followReadAloudPosition) {
|
|
|
|
|
+ readAloudSessionStore.restoreReadAloudFollow()
|
|
|
|
|
+ }
|
|
|
|
|
+ upTextChapterAloudSpan(BaseReadAloudService.currentProgress.coerceAtLeast(0))
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 当前会话是否正指向该 URL 的书。 */
|
|
/** 当前会话是否正指向该 URL 的书。 */
|
|
@@ -273,6 +295,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
renderCallBack?.upPageAnim()
|
|
renderCallBack?.upPageAnim()
|
|
|
upWebBook(book)
|
|
upWebBook(book)
|
|
|
lastBookProgress = null
|
|
lastBookProgress = null
|
|
|
|
|
+ readingAnchorJumpCount = 0
|
|
|
webBookProgress = null
|
|
webBookProgress = null
|
|
|
TextFile.clear()
|
|
TextFile.clear()
|
|
|
requestWholeBookPageEstimate()
|
|
requestWholeBookPageEstimate()
|
|
@@ -584,6 +607,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun setProgress(progress: BookProgress) {
|
|
fun setProgress(progress: BookProgress) {
|
|
|
|
|
+ detachReadAloudFollowIfManual()
|
|
|
if (progress.durChapterIndex < chapterSize &&
|
|
if (progress.durChapterIndex < chapterSize &&
|
|
|
(durChapterIndex != progress.durChapterIndex
|
|
(durChapterIndex != progress.durChapterIndex
|
|
|
|| durChapterPos != progress.durChapterPos)
|
|
|| durChapterPos != progress.durChapterPos)
|
|
@@ -604,11 +628,26 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
lastBookProgress = book?.let { BookProgress(it) }
|
|
lastBookProgress = book?.let { BookProgress(it) }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ fun saveReadingAnchorBeforeChapterJump(targetChapterIndex: Int, targetChapterPos: Int = 0) {
|
|
|
|
|
+ if (!ReadBookConfig.readingAnchorEnabled) return
|
|
|
|
|
+ if (targetChapterIndex == durChapterIndex && targetChapterPos == durChapterPos) return
|
|
|
|
|
+ saveCurrentBookProgress()
|
|
|
|
|
+ if (lastBookProgress != null) readingAnchorJumpCount++
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fun hasReadingAnchor(): Boolean =
|
|
|
|
|
+ ReadBookConfig.readingAnchorEnabled && lastBookProgress != null && readingAnchorJumpCount >= 2
|
|
|
|
|
+
|
|
|
|
|
+ fun discardReadingAnchor() {
|
|
|
|
|
+ lastBookProgress = null
|
|
|
|
|
+ readingAnchorJumpCount = 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
//恢复跳转前进度
|
|
//恢复跳转前进度
|
|
|
fun restoreLastBookProgress() {
|
|
fun restoreLastBookProgress() {
|
|
|
lastBookProgress?.let {
|
|
lastBookProgress?.let {
|
|
|
setProgress(it)
|
|
setProgress(it)
|
|
|
- lastBookProgress = null
|
|
|
|
|
|
|
+ discardReadingAnchor()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -822,7 +861,18 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 用户手动导航(翻页/跳章/拖动进度/滚动)时,页面脱离朗读位置。
|
|
|
|
|
+ * 朗读服务自身驱动的页面移动([BaseReadAloudService.speechDrivingNavigation])不计入。
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun detachReadAloudFollowIfManual() {
|
|
|
|
|
+ if (BaseReadAloudService.isRun && !BaseReadAloudService.speechDrivingNavigation) {
|
|
|
|
|
+ readAloudSessionStore.detachReadAloudFollow()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
fun moveToNextPage(): Boolean {
|
|
fun moveToNextPage(): Boolean {
|
|
|
|
|
+ detachReadAloudFollowIfManual()
|
|
|
var hasNextPage = false
|
|
var hasNextPage = false
|
|
|
curTextChapter?.let {
|
|
curTextChapter?.let {
|
|
|
val nextPagePos = it.getNextPageLength(durChapterPos)
|
|
val nextPagePos = it.getNextPageLength(durChapterPos)
|
|
@@ -840,6 +890,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun moveToPrevPage(): Boolean {
|
|
fun moveToPrevPage(): Boolean {
|
|
|
|
|
+ detachReadAloudFollowIfManual()
|
|
|
var hasPrevPage = false
|
|
var hasPrevPage = false
|
|
|
curTextChapter?.let {
|
|
curTextChapter?.let {
|
|
|
val prevPagePos = it.getPrevPageLength(durChapterPos)
|
|
val prevPagePos = it.getPrevPageLength(durChapterPos)
|
|
@@ -855,6 +906,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun moveToNextChapter(upContent: Boolean, upContentInPlace: Boolean = true): Boolean {
|
|
fun moveToNextChapter(upContent: Boolean, upContentInPlace: Boolean = true): Boolean {
|
|
|
|
|
+ detachReadAloudFollowIfManual()
|
|
|
if (durChapterIndex < simulatedChapterSize - 1) {
|
|
if (durChapterIndex < simulatedChapterSize - 1) {
|
|
|
durChapterPos = 0
|
|
durChapterPos = 0
|
|
|
durChapterIndex++
|
|
durChapterIndex++
|
|
@@ -920,6 +972,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
toLast: Boolean = true,
|
|
toLast: Boolean = true,
|
|
|
upContentInPlace: Boolean = true
|
|
upContentInPlace: Boolean = true
|
|
|
): Boolean {
|
|
): Boolean {
|
|
|
|
|
+ detachReadAloudFollowIfManual()
|
|
|
if (durChapterIndex > 0) {
|
|
if (durChapterIndex > 0) {
|
|
|
durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
|
|
durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
|
|
|
durChapterIndex--
|
|
durChapterIndex--
|
|
@@ -945,6 +998,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun skipToPage(index: Int, success: (() -> Unit)? = null) {
|
|
fun skipToPage(index: Int, success: (() -> Unit)? = null) {
|
|
|
|
|
+ detachReadAloudFollowIfManual()
|
|
|
durChapterPos = curTextChapter?.getReadLength(index) ?: index
|
|
durChapterPos = curTextChapter?.getReadLength(index) ?: index
|
|
|
renderCallBack?.upContent {
|
|
renderCallBack?.upContent {
|
|
|
success?.invoke()
|
|
success?.invoke()
|
|
@@ -955,6 +1009,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fun setPageIndex(index: Int) {
|
|
fun setPageIndex(index: Int) {
|
|
|
|
|
+ detachReadAloudFollowIfManual()
|
|
|
recycleRecorders(durPageIndex, index)
|
|
recycleRecorders(durPageIndex, index)
|
|
|
durChapterPos = curTextChapter?.getReadLength(index) ?: index
|
|
durChapterPos = curTextChapter?.getReadLength(index) ?: index
|
|
|
saveRead(true)
|
|
saveRead(true)
|
|
@@ -983,6 +1038,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
upContent: Boolean = true,
|
|
upContent: Boolean = true,
|
|
|
success: (() -> Unit)? = null
|
|
success: (() -> Unit)? = null
|
|
|
) {
|
|
) {
|
|
|
|
|
+ detachReadAloudFollowIfManual()
|
|
|
// 实时读取章节数而不是依赖缓存 chapterSize:更新目录后 chapterSize 若未同步,
|
|
// 实时读取章节数而不是依赖缓存 chapterSize:更新目录后 chapterSize 若未同步,
|
|
|
// 新增章节的 index 会超出旧值而被下方守卫静默吞掉,表现为「点击新章节无法跳转」。
|
|
// 新增章节的 index 会超出旧值而被下方守卫静默吞掉,表现为「点击新章节无法跳转」。
|
|
|
val chapterCount = book?.bookUrl?.let { appDb.bookChapterDao.getChapterCount(it) }
|
|
val chapterCount = book?.bookUrl?.let { appDb.bookChapterDao.getChapterCount(it) }
|
|
@@ -1010,19 +1066,22 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
renderCallBack?.pageChanged()
|
|
renderCallBack?.pageChanged()
|
|
|
curTextChapter?.let {
|
|
curTextChapter?.let {
|
|
|
if (BaseReadAloudService.isRun && it.isCompleted) {
|
|
if (BaseReadAloudService.isRun && it.isCompleted) {
|
|
|
- if (shouldRestartReadAloudAfterContentLoad(
|
|
|
|
|
- preserveReadAloudPosition = preserveReadAloudPosition,
|
|
|
|
|
- serviceChapterIndex = BaseReadAloudService.currentChapterIndex,
|
|
|
|
|
- loadedChapterIndex = it.chapter.index,
|
|
|
|
|
- )
|
|
|
|
|
- ) {
|
|
|
|
|
- if (isScroll && pageChanged) {
|
|
|
|
|
- ReadAloud.pause(appCtx)
|
|
|
|
|
|
|
+ // 页面已脱离朗读位置(用户手动导航):既不重启朗读到当前页,也不把页面拉回朗读位置
|
|
|
|
|
+ if (readAloudSessionStore.state.value.followReadAloudPosition) {
|
|
|
|
|
+ if (shouldRestartReadAloudAfterContentLoad(
|
|
|
|
|
+ preserveReadAloudPosition = preserveReadAloudPosition,
|
|
|
|
|
+ serviceChapterIndex = BaseReadAloudService.currentChapterIndex,
|
|
|
|
|
+ loadedChapterIndex = it.chapter.index,
|
|
|
|
|
+ )
|
|
|
|
|
+ ) {
|
|
|
|
|
+ if (isScroll && pageChanged) {
|
|
|
|
|
+ ReadAloud.pause(appCtx)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ readAloud(!BaseReadAloudService.pause)
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
- readAloud(!BaseReadAloudService.pause)
|
|
|
|
|
|
|
+ ReadAloud.syncLayout()
|
|
|
}
|
|
}
|
|
|
- } else {
|
|
|
|
|
- ReadAloud.syncLayout()
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1049,6 +1108,20 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
|
|
|
publishSnapshot()
|
|
publishSnapshot()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 把显示页定位到章内字符位置并绘制朗读高亮("回到朗读位置"跳转后调用)。
|
|
|
|
|
+ */
|
|
|
|
|
+ fun upTextChapterAloudSpan(chapterStart: Int) {
|
|
|
|
|
+ if (chapterStart < 0) return
|
|
|
|
|
+ val textChapter = curTextChapter ?: return
|
|
|
|
|
+ if (textChapter.chapter.index != BaseReadAloudService.currentChapterIndex) return
|
|
|
|
|
+ val pageIndex = textChapter.getPageIndexByCharIndex(chapterStart)
|
|
|
|
|
+ if (pageIndex < 0) return
|
|
|
|
|
+ val aloudSpanStart = chapterStart - textChapter.getReadLength(pageIndex)
|
|
|
|
|
+ textChapter.getPage(pageIndex)?.upPageAloudSpan(aloudSpanStart)
|
|
|
|
|
+ renderCallBack?.upContent(resetPageOffset = false)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 当前页数
|
|
* 当前页数
|
|
|
*/
|
|
*/
|