ソースを参照

fix: 修复听书播放器朗读设置跳转

lingran7031 1 ヶ月 前
親
コミット
db81ec96a0

+ 150 - 6
app/src/main/java/io/legado/app/ui/book/readaloud/ReadAloudPlayerMorphHost.kt

@@ -6,17 +6,23 @@ import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.core.ui.player.PlayerMorphAppearance
 import io.legado.app.core.ui.player.PlayerMorphHost
+import io.legado.app.help.IntentHelp
 import io.legado.app.ui.book.read.sheet.ReadAloudConfigContent
+import io.legado.app.ui.book.read.sheet.ReadAloudNumberConfigSheet
 import io.legado.app.ui.book.read.sheet.asReadBookUiState
 import io.legado.app.ui.book.readaloud.morph.ReadAloudMorphState
+import io.legado.app.ui.book.readaloud.player.ReadAloudConfigOption
+import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerConfigHostAction
 import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerEffect
 import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerScreenContent
 import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerUiState
@@ -24,6 +30,8 @@ import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerViewModel
 import io.legado.app.ui.book.readaloud.player.applyReadBookConfigIntent
 import io.legado.app.ui.book.readaloud.player.rememberPlayerThemeOverride
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.utils.toastOnUi
+import kotlinx.coroutines.launch
 
 /** 朗读特有的设置与经典控制;几何、封面和返回手势由共用宿主处理。 */
 @Composable
@@ -36,22 +44,83 @@ fun ReadAloudPlayerMorphHost(
     predictiveBackEnabled: Boolean = true,
     onDismiss: () -> Unit,
     onSwitchToClassic: (bookUrl: String) -> Unit,
+    onOpenTtsEnginesAndVoices: (bookUrl: String) -> Unit,
+    onOpenTtsCache: () -> Unit,
+    onOpenBookVoiceCasting: (bookUrl: String) -> Unit,
 ) {
+    val context = LocalContext.current
+    val scope = rememberCoroutineScope()
     val settingsState by playerViewModel.readAloudSettings.collectAsStateWithLifecycle()
     var configVisible by rememberSaveable { mutableStateOf(false) }
+    var activeNumberConfig by rememberSaveable {
+        mutableStateOf<ReadAloudPlayerConfigHostAction?>(null)
+    }
     val expanded by remember { derivedStateOf { morph.expanded } }
     val currentDismiss by rememberUpdatedState(onDismiss)
     val currentSwitch by rememberUpdatedState(onSwitchToClassic)
-    LaunchedEffect(expanded) { if (!expanded) configVisible = false }
+    val currentOpenTtsEnginesAndVoices by rememberUpdatedState(onOpenTtsEnginesAndVoices)
+    val currentOpenTtsCache by rememberUpdatedState(onOpenTtsCache)
+    val currentOpenBookVoiceCasting by rememberUpdatedState(onOpenBookVoiceCasting)
+
+    suspend fun collapsePlayer() {
+        configVisible = false
+        activeNumberConfig = null
+        morph.animateTo(0f)
+        currentDismiss()
+    }
+
+    fun navigateFromPlayer(action: () -> Unit) {
+        scope.launch {
+            collapsePlayer()
+            action()
+        }
+    }
+
+    fun showNumberConfig(action: ReadAloudPlayerConfigHostAction) {
+        configVisible = false
+        activeNumberConfig = action
+    }
+
+    fun handleHostAction(action: ReadAloudPlayerConfigHostAction) {
+        when (action) {
+            ReadAloudPlayerConfigHostAction.OpenTtsEnginesAndVoices -> navigateFromPlayer {
+                currentOpenTtsEnginesAndVoices(playerViewModel.uiState.value.bookUrl)
+            }
+
+            ReadAloudPlayerConfigHostAction.OpenTtsCache -> navigateFromPlayer {
+                currentOpenTtsCache()
+            }
+
+            ReadAloudPlayerConfigHostAction.OpenBookVoiceCasting -> navigateFromPlayer {
+                currentOpenBookVoiceCasting(playerViewModel.uiState.value.bookUrl)
+            }
+
+            ReadAloudPlayerConfigHostAction.OpenSystemTtsSettings ->
+                IntentHelp.openTTSSetting()
+
+            ReadAloudPlayerConfigHostAction.OpenPreDownloadNumPicker,
+            ReadAloudPlayerConfigHostAction.OpenPreSynthesisConcurrencyPicker,
+            ReadAloudPlayerConfigHostAction.OpenParagraphIntervalPicker,
+            ReadAloudPlayerConfigHostAction.OpenCacheCleanTimePicker -> showNumberConfig(action)
+        }
+    }
+
+    LaunchedEffect(expanded) {
+        if (!expanded) {
+            configVisible = false
+            activeNumberConfig = null
+        }
+    }
     LaunchedEffect(playerViewModel) {
         playerViewModel.effects.collect { effect ->
             when (effect) {
                 ReadAloudPlayerEffect.ReturnToClassic -> {
-                    configVisible = false
-                    morph.animateTo(0f)
-                    currentDismiss()
+                    collapsePlayer()
                     currentSwitch(playerViewModel.uiState.value.bookUrl)
                 }
+
+                ReadAloudPlayerEffect.TtsCacheCleared ->
+                    context.toastOnUi(R.string.clear_cache_success)
             }
         }
     }
@@ -65,7 +134,8 @@ fun ReadAloudPlayerMorphHost(
         visible = visible,
         awaitCapsuleAnchor = awaitCapsuleAnchor,
         predictiveBackEnabled = predictiveBackEnabled,
-        backEnabled = !configVisible && playerState.activeSheet == null,
+        backEnabled = !configVisible && activeNumberConfig == null &&
+                playerState.activeSheet == null,
         onDismiss = onDismiss,
     ) { onCollapse ->
         ReadAloudPlayerScreenContent(
@@ -83,8 +153,82 @@ fun ReadAloudPlayerMorphHost(
         ReadAloudConfigContent(
             state = settingsState.asReadBookUiState(),
             playerState = playerState,
-            onIntent = playerViewModel::applyReadBookConfigIntent,
+            onIntent = { intent ->
+                playerViewModel.applyReadBookConfigIntent(intent, ::handleHostAction)
+            },
             onPlayerIntent = playerViewModel::onIntent,
         )
     }
+
+    val dismissNumberConfig = {
+        activeNumberConfig = null
+        configVisible = true
+    }
+    ReadAloudNumberConfigSheet(
+        show = expanded &&
+                activeNumberConfig == ReadAloudPlayerConfigHostAction.OpenPreDownloadNumPicker,
+        title = stringResource(R.string.read_aloud_preload),
+        description = stringResource(
+            R.string.read_aloud_preload_summary,
+            settingsState.preDownloadNum,
+        ),
+        value = settingsState.preDownloadNum,
+        defaultValue = 10,
+        valueRange = 0f..100f,
+        onValueChange = {
+            playerViewModel.onConfigIntent(ReadAloudConfigOption.PreDownloadNum, intValue = it)
+        },
+        onDismissRequest = dismissNumberConfig,
+    )
+    ReadAloudNumberConfigSheet(
+        show = expanded && activeNumberConfig ==
+                ReadAloudPlayerConfigHostAction.OpenPreSynthesisConcurrencyPicker,
+        title = stringResource(R.string.tts_pre_synthesis_concurrency),
+        description = stringResource(
+            R.string.tts_pre_synthesis_concurrency_summary,
+            settingsState.preSynthesisConcurrency,
+        ),
+        value = settingsState.preSynthesisConcurrency,
+        defaultValue = 3,
+        valueRange = 1f..8f,
+        onValueChange = {
+            playerViewModel.onConfigIntent(
+                ReadAloudConfigOption.PreSynthesisConcurrency,
+                intValue = it,
+            )
+        },
+        onDismissRequest = dismissNumberConfig,
+    )
+    ReadAloudNumberConfigSheet(
+        show = expanded &&
+                activeNumberConfig == ReadAloudPlayerConfigHostAction.OpenParagraphIntervalPicker,
+        title = stringResource(R.string.tts_paragraph_interval),
+        description = stringResource(
+            R.string.tts_paragraph_interval_summary,
+            settingsState.readAloudParagraphInterval,
+        ),
+        value = settingsState.readAloudParagraphInterval,
+        defaultValue = 0,
+        valueRange = 0f..5000f,
+        onValueChange = {
+            playerViewModel.onConfigIntent(ReadAloudConfigOption.ParagraphInterval, intValue = it)
+        },
+        onDismissRequest = dismissNumberConfig,
+    )
+    ReadAloudNumberConfigSheet(
+        show = expanded &&
+                activeNumberConfig == ReadAloudPlayerConfigHostAction.OpenCacheCleanTimePicker,
+        title = stringResource(R.string.audio_cache_clean_time),
+        description = stringResource(
+            R.string.audio_cache_clean_time_summary,
+            settingsState.audioCacheCleanTime,
+        ),
+        value = settingsState.audioCacheCleanTime,
+        defaultValue = 10,
+        valueRange = 0f..10080f,
+        onValueChange = {
+            playerViewModel.onConfigIntent(ReadAloudConfigOption.AudioCacheCleanTime, intValue = it)
+        },
+        onDismissRequest = dismissNumberConfig,
+    )
 }

+ 58 - 4
app/src/main/java/io/legado/app/ui/book/readaloud/player/ReadAloudConfigIntentMapper.kt

@@ -2,14 +2,67 @@ package io.legado.app.ui.book.readaloud.player
 
 import io.legado.app.ui.book.read.ReadBookIntent
 
+internal enum class ReadAloudPlayerConfigHostAction {
+    OpenTtsEnginesAndVoices,
+    OpenTtsCache,
+    OpenBookVoiceCasting,
+    OpenSystemTtsSettings,
+    OpenPreDownloadNumPicker,
+    OpenPreSynthesisConcurrencyPicker,
+    OpenParagraphIntervalPicker,
+    OpenCacheCleanTimePicker,
+}
+
 /**
- * 把配置卡片的 [ReadBookIntent] 翻译成全局设置写入。
+ * 为 `ReadAloudConfigContent` 发出的每一个导航选项,返回对应的播放器宿主操作。
+ *
+ * 共享内容使用的是 [ReadBookIntent],而播放器是一个 Activity 级别的浮层,
+ * 没有阅读器的 ViewModel。将这个桥接逻辑保持为纯函数,
+ * 可以让这些点击事件被转发出去,而不是落到设置适配器里。
+ */
+internal fun ReadBookIntent.toReadAloudPlayerConfigHostAction():
+        ReadAloudPlayerConfigHostAction? =
+    when (this) {
+        ReadBookIntent.OpenTtsEnginesAndVoices ->
+            ReadAloudPlayerConfigHostAction.OpenTtsEnginesAndVoices
+
+        ReadBookIntent.OpenTtsCache -> ReadAloudPlayerConfigHostAction.OpenTtsCache
+        ReadBookIntent.OpenBookVoiceCasting ->
+            ReadAloudPlayerConfigHostAction.OpenBookVoiceCasting
+
+        ReadBookIntent.OpenSystemTtsSettings ->
+            ReadAloudPlayerConfigHostAction.OpenSystemTtsSettings
+
+        ReadBookIntent.OpenPreDownloadNumPicker ->
+            ReadAloudPlayerConfigHostAction.OpenPreDownloadNumPicker
+
+        ReadBookIntent.OpenPreSynthesisConcurrencyPicker ->
+            ReadAloudPlayerConfigHostAction.OpenPreSynthesisConcurrencyPicker
+
+        ReadBookIntent.OpenParagraphIntervalPicker ->
+            ReadAloudPlayerConfigHostAction.OpenParagraphIntervalPicker
+
+        ReadBookIntent.OpenCacheCleanTimePicker ->
+            ReadAloudPlayerConfigHostAction.OpenCacheCleanTimePicker
+
+        else -> null
+    }
+
+/**
+ * 把配置卡片的 [ReadBookIntent] 分发为全局设置写入、播放器命令或宿主动作。
  *
  * 配置内容(`ReadAloudConfigContent`)的契约是 `ReadBookIntent`,因为它的主宿主是阅读器。
  * 听书播放弹层是全局浮层、没有阅读器 ViewModel,所以这里把同一批意图落到
- * [ReadAloudPlayerViewModel.onConfigIntent],两个宿主的设置语义完全一致。
+ * [ReadAloudPlayerViewModel.onConfigIntent] 或播放器宿主,两个宿主的设置语义完全一致。
  */
-internal fun ReadAloudPlayerViewModel.applyReadBookConfigIntent(intent: ReadBookIntent) {
+internal fun ReadAloudPlayerViewModel.applyReadBookConfigIntent(
+    intent: ReadBookIntent,
+    onHostAction: (ReadAloudPlayerConfigHostAction) -> Unit,
+) {
+    intent.toReadAloudPlayerConfigHostAction()?.let {
+        onHostAction(it)
+        return
+    }
     when (intent) {
         is ReadBookIntent.SetDefaultReadAloudInterface ->
             onConfigIntent(ReadAloudConfigOption.DefaultInterface, value = intent.value)
@@ -68,7 +121,8 @@ internal fun ReadAloudPlayerViewModel.applyReadBookConfigIntent(intent: ReadBook
         is ReadBookIntent.ApplyAudioCacheCleanTime ->
             onConfigIntent(ReadAloudConfigOption.AudioCacheCleanTime, intValue = intent.value)
 
-        // 其余意图在播放界面没有等价动作(缓存清理、数值选择器弹层等),静默忽略
+        ReadBookIntent.ResetReadAloudCapsulePosition -> resetCapsulePosition()
+        ReadBookIntent.ClearTtsCache -> clearTtsCache()
         else -> Unit
     }
 }

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/readaloud/player/ReadAloudPlayerContract.kt

@@ -88,4 +88,5 @@ sealed interface ReadAloudPlayerIntent {
 
 sealed interface ReadAloudPlayerEffect {
     data object ReturnToClassic : ReadAloudPlayerEffect
+    data object TtsCacheCleared : ReadAloudPlayerEffect
 }

+ 7 - 0
app/src/main/java/io/legado/app/ui/book/readaloud/player/ReadAloudPlayerCoordinator.kt

@@ -18,9 +18,11 @@ import io.legado.app.model.ReadBook
 import io.legado.app.model.reader.ReaderChapterInput
 import io.legado.app.service.BaseReadAloudService
 import io.legado.app.ui.book.read.ReadConfigUpdateBus
+import io.legado.app.utils.TTSCacheUtils
 import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -32,6 +34,7 @@ import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.withContext
 import kotlin.math.roundToInt
 
 /** Compatibility boundary between the Compose player and the legacy reader/service state. */
@@ -227,6 +230,10 @@ class ReadAloudPlayerCoordinator(
     fun nextChapter() = ReadBook.moveToNextChapter(true)
     fun selectChapter(index: Int) = ReadBook.openChapter(index, durChapterPos = 0)
 
+    suspend fun clearTtsCache() = withContext(Dispatchers.IO) {
+        TTSCacheUtils.clearTtsCache()
+    }
+
     suspend fun setSpeed(value: Int) {
         readAloudSettingsGateway.update { it.copy(ttsSpeechRate = coerceReadAloudSpeed(value)) }
         ReadAloud.upTtsSpeechRate(application)

+ 15 - 0
app/src/main/java/io/legado/app/ui/book/readaloud/player/ReadAloudPlayerViewModel.kt

@@ -195,6 +195,21 @@ class ReadAloudPlayerViewModel(
         }
     }
 
+    internal fun resetCapsulePosition() {
+        viewModelScope.launch {
+            readAloudSettingsGateway.update {
+                it.copy(capsuleOffsetX = 0f, capsuleOffsetY = 0f)
+            }
+        }
+    }
+
+    internal fun clearTtsCache() {
+        viewModelScope.launch {
+            coordinator.clearTtsCache()
+            effect(ReadAloudPlayerEffect.TtsCacheCleared)
+        }
+    }
+
     private fun cycleBgMode() {
         val next = when (readBgMode()) {
             ReadAloudBgMode.Solid -> ReadAloudBgMode.Blur

+ 23 - 0
app/src/main/java/io/legado/app/ui/main/MainActivity.kt

@@ -758,6 +758,29 @@ open class MainActivity : BaseComposeActivity(), AudioPlay.CallBack {
                                 )
                             }
                         },
+                        onOpenTtsEnginesAndVoices = { bookUrl ->
+                            MainNavigator.navigateToRoute(
+                                backStack,
+                                MainRouteCloudTtsEngines(bookUrl.takeIf(String::isNotBlank)),
+                                navRouteTracker,
+                            )
+                        },
+                        onOpenTtsCache = {
+                            MainNavigator.navigateToRoute(
+                                backStack,
+                                MainRouteTtsCache,
+                                navRouteTracker,
+                            )
+                        },
+                        onOpenBookVoiceCasting = { bookUrl ->
+                            if (bookUrl.isNotBlank()) {
+                                MainNavigator.navigateToRoute(
+                                    backStack,
+                                    MainRouteBookVoiceCasting(bookUrl),
+                                    navRouteTracker,
+                                )
+                            }
+                        },
                     )
                     if (playerSource == PlaybackCapsuleSource.AudioBook &&
                         (audioPlayerVisible || morphPresent)