HapeLee 2 месяцев назад
Родитель
Сommit
d762fcaa32

+ 4 - 1
app/src/main/java/io/legado/app/service/AudioPlayService.kt

@@ -21,6 +21,7 @@ import androidx.lifecycle.lifecycleScope
 import androidx.media.AudioFocusRequestCompat
 import androidx.media3.common.PlaybackException
 import androidx.media3.common.Player
+import androidx.media3.common.util.UnstableApi
 import androidx.media3.exoplayer.ExoPlayer
 import io.legado.app.R
 import io.legado.app.base.BaseService
@@ -57,6 +58,7 @@ import splitties.systemservices.audioManager
 import splitties.systemservices.notificationManager
 import splitties.systemservices.powerManager
 import splitties.systemservices.wifiManager
+import kotlin.time.Duration.Companion.milliseconds
 
 /**
  * 音频播放服务
@@ -457,7 +459,7 @@ class AudioPlayService : BaseService(),
                         AudioPlay.next()
                     }
                 }
-                delay(1000)
+                delay(1000.milliseconds)
             }
         }
     }
@@ -465,6 +467,7 @@ class AudioPlayService : BaseService(),
     /**
      * 应用音频增益(LoudnessEnhancer,单位 mB,范围 -6000..6000)
      */
+    @androidx.annotation.OptIn(UnstableApi::class)
     private fun applyGain(gain: Int) {
         try {
             if (gain == 0) {

+ 22 - 1
app/src/main/java/io/legado/app/ui/book/read/ReadAloudDelegate.kt

@@ -11,10 +11,10 @@ import io.legado.app.data.repository.ReadSettingsRepository
 import io.legado.app.domain.gateway.AiProfileGateway
 import io.legado.app.domain.model.AiTaskType
 import io.legado.app.domain.model.PlaybackTimer
-import io.legado.app.domain.model.settings.ReadAloudSettings
 import io.legado.app.domain.model.readaloud.ReadAloudSessionStatus
 import io.legado.app.domain.model.readaloud.ReadAloudVoice
 import io.legado.app.domain.model.readaloud.VoiceCatalogEntry
+import io.legado.app.domain.model.settings.ReadAloudSettings
 import io.legado.app.domain.usecase.SyncReadAloudVoicesUseCase
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadAloudSessionStore
@@ -175,6 +175,27 @@ class ReadAloudDelegate(
         ReadBook.moveToNextChapter(true)
     }
 
+    /** 回到朗读位置:恢复页面跟随朗读,并跳到朗读所在章节/字符位置。全程不打断当前朗读。 */
+    fun backToSpeakingPosition() {
+        readAloudSessionStore.restoreReadAloudFollow()
+        val speakingChapterIndex = BaseReadAloudService.currentChapterIndex
+        val speakingChapterStart = BaseReadAloudService.currentProgress
+        if (speakingChapterIndex < 0) return
+        // currentProgress 为正在朗读的精确章内位置(onRangeStart 段内偏移上报),整段高亮落在当前段
+        val chapterStart = speakingChapterStart.coerceAtLeast(0)
+        if (speakingChapterIndex != ReadBook.durChapterIndex) {
+            // 跳到朗读位置属于朗读相关的页面移动,不能触发手动脱离
+            BaseReadAloudService.withSpeechNavigation {
+                ReadBook.openChapter(speakingChapterIndex, chapterStart) {
+                    ReadBook.upTextChapterAloudSpan(chapterStart)
+                }
+            }
+        } else {
+            ReadBook.syncReadAloudPage(speakingChapterIndex, chapterStart)
+            ReadBook.upTextChapterAloudSpan(chapterStart)
+        }
+    }
+
     // --- 界面入口 ---
 
     /** 媒体键/胶囊触发的默认朗读界面:按设置决定开播放器弹层还是经典控制面板。 */

+ 1 - 24
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -1345,7 +1345,7 @@ class ReadBookViewModel(
             is ReadBookIntent.ReadAloudNextParagraph -> readAloudDelegate.nextParagraph()
             is ReadBookIntent.ReadAloudPrevChapter -> readAloudDelegate.prevChapter()
             is ReadBookIntent.ReadAloudNextChapter -> readAloudDelegate.nextChapter()
-            ReadBookIntent.BackToSpeakingPosition -> backToSpeakingPosition()
+            ReadBookIntent.BackToSpeakingPosition -> readAloudDelegate.backToSpeakingPosition()
             ReadBookIntent.ReadAloudFromHere -> ReadBook.readAloud()
             is ReadBookIntent.SetReadAloudTtsTimer -> readAloudDelegate.setTtsTimer(intent.value)
             is ReadBookIntent.SetFinishCurrentChapterAfterTimer ->
@@ -2137,29 +2137,6 @@ class ReadBookViewModel(
         return bookRepository.getChapter(book.bookUrl, ReadBook.durChapterIndex)
     }
 
-    /**
-     * 回到朗读位置:恢复页面跟随朗读,并跳到朗读所在章节/字符位置。全程不打断当前朗读。
-     */
-    private fun backToSpeakingPosition() {
-        readAloudSessionStore.restoreReadAloudFollow()
-        val speakingChapterIndex = BaseReadAloudService.currentChapterIndex
-        val speakingChapterStart = BaseReadAloudService.currentProgress
-        if (speakingChapterIndex < 0) return
-        // currentProgress 为正在朗读的精确章内位置(onRangeStart 段内偏移上报),整段高亮落在当前段
-        val chapterStart = speakingChapterStart.coerceAtLeast(0)
-        if (speakingChapterIndex != ReadBook.durChapterIndex) {
-            // 跳到朗读位置属于朗读相关的页面移动,不能触发手动脱离
-            BaseReadAloudService.withSpeechNavigation {
-                ReadBook.openChapter(speakingChapterIndex, chapterStart) {
-                    ReadBook.upTextChapterAloudSpan(chapterStart)
-                }
-            }
-        } else {
-            ReadBook.syncReadAloudPage(speakingChapterIndex, chapterStart)
-            ReadBook.upTextChapterAloudSpan(chapterStart)
-        }
-    }
-
     // 开书 / 目录 / 换源 / 进度同步已迁入 [ReadBookLoadDelegate],这里只留外部入口的转发。
 
     suspend fun initReadBookConfig(request: ReadBookInitRequest) =

+ 2 - 0
app/src/test/java/io/legado/app/ui/book/read/ConfigUpdateActionsInvariantTest.kt

@@ -151,6 +151,8 @@ class ConfigUpdateActionsInvariantTest {
             "UseNewTocSheet",
             // 只影响下次目录解析时的无规则章节切分长度,取用时读设置,无渲染副作用
             "MaxLengthWithNoToc",
+            // 只写 DataStore 的 readingAnchorEnabled,锚点栏可见性由 readPreferences 反应式驱动
+            "ReadingAnchorEnabled",
         )
     }
 }

+ 17 - 3
app/src/test/java/io/legado/app/ui/book/read/ReadBookDomainSplitBoundaryTest.kt

@@ -137,16 +137,30 @@ class ReadBookDomainSplitBoundaryTest {
      * 合并自 PR #2024。朗读域早已是 `ReadAloudDelegate`——留在 VM 的只有两条 `when`
      * 分支转发,各两行(分支 + 转发),与上方 `SetReadAloudSystemMediaCompat` 等兄弟
      * 分支同款,逐行都摘不掉:意图入口只能在 VM。
+     *
+     * 2668 → 2674:合并 PR #2089(阅读锚点)。新增 6 行全部是纯接线,逻辑在
+     * `ReadBook`(模型层)与 `ReadAloudDelegate`,逐行都摘不掉:
+     *
+     * - 阅读锚点:`OpenChapter`/`OpenChapterResult`/`SeekToChapter` 三个意图分支前置
+     *   `saveReadingAnchorBeforeChapterJump()`、`RestoreLastBookProgress`/`KeepCurrentBookProgress`
+     *   恢复/丢弃锚点并 `syncFromReadBook` 刷新锚点可用状态、`syncFromReadBook` 投影
+     *   `readingAnchorAvailable`——锚点状态与跳转计数住在 `ReadBook`,UiState 与
+     *   `_uiState.update` 只有 VM 能碰,意图入口与状态投影只能在 VM。
+     * - 朗读域:`ReadAloudFromHere` 意图分支一行转发 `ReadBook.readAloud()`、
+     *   `BackToSpeakingPosition` 转发 `ReadAloudDelegate.backToSpeakingPosition()`、
+     *   会话快照投影 `readAloudFollow`——与既有朗读分支同款。
+     * - `backToSpeakingPosition()` 本体(恢复跟随 + 跳章/跳字符)已下沉到
+     *   `ReadAloudDelegate`,未占本线额度。
      */
     @Test
-    fun `ReadBookViewModel 不超过 R2 验收的 2668 行`() {
+    fun `ReadBookViewModel 不超过 R2 验收的 2674 行`() {
         val lineCount = mainSourceFile("io/legado/app/ui/book/read/ReadBookViewModel.kt")
             .readLines().size
         assertTrue(
-            "ReadBookViewModel 涨到了 $lineCount 行,超过 R2 验收线 2668。\n" +
+            "ReadBookViewModel 涨到了 $lineCount 行,超过 R2 验收线 2674。\n" +
                 "新功能请摘成 io/legado/app/ui/book/read/ 下的 XxxDelegate," +
                 "并在本测试的 DOMAINS 里加一条边界。",
-            lineCount <= 2668,
+            lineCount <= 2674,
         )
     }