Просмотр исходного кода

refactor: 迁移 ReadAloud 设置网关

5151561 3 месяцев назад
Родитель
Сommit
3715a3080c

+ 102 - 204
app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt

@@ -3,10 +3,10 @@ package io.legado.app.data.repository
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.booleanPreferencesKey
 import androidx.datastore.preferences.core.floatPreferencesKey
+import androidx.datastore.preferences.core.intPreferencesKey
 import androidx.datastore.preferences.core.stringPreferencesKey
 import io.legado.app.constant.PreferKey
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
-import io.legado.app.domain.gateway.ReadAloudSettingsUpdate
 import io.legado.app.domain.model.PlaybackTimer
 import io.legado.app.domain.model.settings.ReadAloudSettings
 import io.legado.app.help.config.AppConfigStore
@@ -15,222 +15,120 @@ import io.legado.app.help.config.compatDsValue
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
 
-typealias ReadAloudPreferences = ReadAloudSettings
-
-class ReadAloudSettingsRepository(
-    private val settingsRepository: SettingsRepository
-) : ReadAloudSettingsGateway {
+class ReadAloudSettingsRepository : ReadAloudSettingsGateway {
 
     override val currentSettings: ReadAloudSettings
-        get() = AppConfigStore.preferences.toReadAloudPreferences()
+        get() = AppConfigStore.preferences.toReadAloudSettings()
 
     override val settings: Flow<ReadAloudSettings> = AppConfigStore.preferencesFlow
         .map { preferences ->
-            preferences.toReadAloudPreferences()
-        }
-    val preferences: Flow<ReadAloudPreferences> = settings
-
-    override suspend fun update(update: ReadAloudSettingsUpdate) {
-        when (update) {
-            is ReadAloudSettingsUpdate.TtsEngine -> setTtsEngine(update.value)
-            is ReadAloudSettingsUpdate.ParagraphInterval -> setTtsParagraphInterval(update.value)
-            is ReadAloudSettingsUpdate.AudioCacheCleanTime -> setAudioCacheCleanTime(update.value)
-            is ReadAloudSettingsUpdate.IgnoreAudioFocus -> setIgnoreAudioFocus(update.value)
-            is ReadAloudSettingsUpdate.MediaButtonOnExit -> setMediaButtonOnExit(update.value)
-            is ReadAloudSettingsUpdate.ReadAloudByMediaButton -> setReadAloudByMediaButton(update.value)
-            is ReadAloudSettingsUpdate.PauseWhilePhoneCalls -> setPauseReadAloudWhilePhoneCalls(update.value)
-            is ReadAloudSettingsUpdate.ReadAloudWakeLock -> setReadAloudWakeLock(update.value)
-            is ReadAloudSettingsUpdate.MediaButtonPerNext -> setMediaButtonPerNext(update.value)
-            is ReadAloudSettingsUpdate.ReadAloudByPage -> setReadAloudByPage(update.value)
-            is ReadAloudSettingsUpdate.SystemMediaControlCompatibility ->
-                setSystemMediaControlCompatibilityChange(update.value)
-            is ReadAloudSettingsUpdate.StreamAudio -> setStreamReadAloudAudio(update.value)
-            is ReadAloudSettingsUpdate.Timer -> setTtsTimer(update.value)
-            is ReadAloudSettingsUpdate.FollowSystem -> setTtsFollowSys(update.value)
-            is ReadAloudSettingsUpdate.SpeechRate -> setTtsSpeechRate(update.value)
-            is ReadAloudSettingsUpdate.ContentSelectSpeakMode ->
-                settingsRepository.putInt(PreferKey.contentSelectSpeakMod, update.value)
-            is ReadAloudSettingsUpdate.AudioPreDownloadNum ->
-                settingsRepository.putInt(PreferKey.audioPreDownloadNum, update.value)
-            is ReadAloudSettingsUpdate.PreSynthesisConcurrency ->
-                settingsRepository.putInt(PreferKey.ttsPreSynthesisConcurrency, update.value)
-
-            is ReadAloudSettingsUpdate.CapsuleAutoCollapse ->
-                settingsRepository.putBoolean(PreferKey.capsuleAutoCollapse, update.value)
+            preferences.toReadAloudSettings()
         }
-    }
-
-    suspend fun setTtsEngine(value: String?) {
-        AppConfigStore.putString(PreferKey.ttsEngine, value)
-    }
-
-    suspend fun setTtsParagraphInterval(value: Int) {
-        settingsRepository.putInt(PreferKey.ttsParagraphInterval, value)
-    }
-
-    suspend fun setAudioCacheCleanTime(value: Int) {
-        settingsRepository.putInt(PreferKey.audioCacheCleanTime, value)
-    }
-
-    suspend fun setIgnoreAudioFocus(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.ignoreAudioFocus, value)
-    }
-
-    suspend fun setMediaButtonOnExit(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.mediaButtonOnExit, value)
-    }
+    val preferences: Flow<ReadAloudSettings> = settings
 
-    suspend fun setReadAloudByMediaButton(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.readAloudByMediaButton, value)
-    }
-
-    suspend fun setPauseReadAloudWhilePhoneCalls(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.pauseReadAloudWhilePhoneCalls, value)
-    }
-
-    suspend fun setReadAloudWakeLock(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.readAloudWakeLock, value)
-    }
-
-    suspend fun setShowReadAloudCapsule(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.showReadAloudCapsule, value)
-    }
-
-    suspend fun setCapsulePosition(x: Float, y: Float) {
-        settingsRepository.putFloat(Keys.CapsuleOffsetX.name, x)
-        settingsRepository.putFloat(Keys.CapsuleOffsetY.name, y)
-    }
-
-    suspend fun resetCapsulePosition() = setCapsulePosition(0f, 0f)
-
-    suspend fun setMediaButtonPerNext(value: Boolean) {
-        settingsRepository.putBoolean(KEY_MEDIA_BUTTON_PER_NEXT, value)
-    }
-
-    suspend fun setReadAloudByPage(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.readAloudByPage, value)
-    }
-
-    suspend fun setSystemMediaControlCompatibilityChange(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.systemMediaControlCompatibilityChange, value)
-    }
-
-    suspend fun setStreamReadAloudAudio(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.streamReadAloudAudio, value)
-    }
-
-    suspend fun setTtsTimer(value: Int) {
-        val timer = PlaybackTimer.normalize(value)
-        settingsRepository.putInt(PreferKey.ttsTimer, timer)
-    }
-
-    suspend fun setTtsFollowSys(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.ttsFollowSys, value)
-    }
-
-    suspend fun setTtsSpeechRate(value: Int) {
-        settingsRepository.putInt(PreferKey.ttsSpeechRate, value.coerceIn(0, 80))
-    }
-
-    suspend fun setSpeechAnalysisMode(value: String) {
-        settingsRepository.putString(PreferKey.speechAnalysisMode, value)
-    }
-
-    suspend fun setUseMultiSpeaker(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.useMultiSpeaker, value)
-    }
-
-    suspend fun setDefaultInterface(value: String) {
-        settingsRepository.putString(
-            PreferKey.defaultReadAloudInterface,
-            value.takeIf { it in AVAILABLE_INTERFACES } ?: DEFAULT_INTERFACE_CLASSIC,
-        )
-    }
-
-    suspend fun setPreSynthesisConcurrency(value: Int) {
-        settingsRepository.putInt(PreferKey.ttsPreSynthesisConcurrency, value.coerceIn(1, 8))
-    }
-
-    suspend fun setCapsuleAutoCollapse(value: Boolean) {
-        settingsRepository.putBoolean(PreferKey.capsuleAutoCollapse, value)
-    }
-
-    private fun Preferences.toReadAloudPreferences(): ReadAloudPreferences {
-        return ReadAloudPreferences(
-            ttsEngine = compatDsString(PreferKey.ttsEngine),
-            ttsParagraphInterval = compatDsValue(Keys.TtsParagraphInterval, 0),
-            audioCacheCleanTime = compatDsValue(Keys.AudioCacheCleanTime, 10),
-            ignoreAudioFocus = compatDsValue(Keys.IgnoreAudioFocus, false),
-            mediaButtonOnExit = compatDsValue(Keys.MediaButtonOnExit, true),
-            readAloudByMediaButton = compatDsValue(Keys.ReadAloudByMediaButton, false),
-            pauseReadAloudWhilePhoneCalls = compatDsValue(Keys.PauseReadAloudWhilePhoneCalls, false),
-            readAloudWakeLock = compatDsValue(Keys.ReadAloudWakeLock, false),
-            showReadAloudCapsule = compatDsValue(Keys.ShowReadAloudCapsule, true),
-            capsuleAutoCollapse = compatDsValue(Keys.CapsuleAutoCollapse, true),
-            capsuleOffsetX = compatDsValue(Keys.CapsuleOffsetX, 0f),
-            capsuleOffsetY = compatDsValue(Keys.CapsuleOffsetY, 0f),
-            mediaButtonPerNext = compatDsValue(Keys.MediaButtonPerNext, false),
-            readAloudByPage = compatDsValue(Keys.ReadAloudByPage, false),
-            systemMediaControlCompatibilityChange =
-                compatDsValue(Keys.SystemMediaControlCompatibilityChange, true),
-            streamReadAloudAudio = compatDsValue(Keys.StreamReadAloudAudio, false),
-            ttsTimer = PlaybackTimer.normalize(compatDsValue(Keys.TtsTimer, 0)),
-            ttsFollowSys = compatDsValue(Keys.TtsFollowSys, true),
-            ttsSpeechRate = compatDsValue(Keys.TtsSpeechRate, 5),
-            speechAnalysisMode = compatDsValue(Keys.SpeechAnalysisMode, "rule"),
-            useMultiSpeaker = compatDsValue(Keys.UseMultiSpeaker, true),
-            defaultInterface = compatDsValue(Keys.DefaultInterface, DEFAULT_INTERFACE_CLASSIC),
-            contentSelectSpeakMode = compatDsValue(Keys.ContentSelectSpeakMode, 0),
-            audioPreDownloadNum = compatDsValue(Keys.AudioPreDownloadNum, 10),
-            ttsPreSynthesisConcurrency = compatDsValue(Keys.PreSynthesisConcurrency, 3),
-        )
-    }
-
-    private object Keys {
-        val TtsParagraphInterval = androidx.datastore.preferences.core.intPreferencesKey(
-            PreferKey.ttsParagraphInterval
-        )
-        val AudioCacheCleanTime = androidx.datastore.preferences.core.intPreferencesKey(
-            PreferKey.audioCacheCleanTime
-        )
-        val IgnoreAudioFocus = booleanPreferencesKey(PreferKey.ignoreAudioFocus)
-        val MediaButtonOnExit = booleanPreferencesKey(PreferKey.mediaButtonOnExit)
-        val ReadAloudByMediaButton = booleanPreferencesKey(PreferKey.readAloudByMediaButton)
-        val PauseReadAloudWhilePhoneCalls =
-            booleanPreferencesKey(PreferKey.pauseReadAloudWhilePhoneCalls)
-        val ReadAloudWakeLock = booleanPreferencesKey(PreferKey.readAloudWakeLock)
-        val ShowReadAloudCapsule = booleanPreferencesKey(PreferKey.showReadAloudCapsule)
-        val CapsuleAutoCollapse = booleanPreferencesKey(PreferKey.capsuleAutoCollapse)
-        val CapsuleOffsetX = floatPreferencesKey("read_aloud_capsule_offset_x")
-        val CapsuleOffsetY = floatPreferencesKey("read_aloud_capsule_offset_y")
-        val MediaButtonPerNext = booleanPreferencesKey(KEY_MEDIA_BUTTON_PER_NEXT)
-        val ReadAloudByPage = booleanPreferencesKey(PreferKey.readAloudByPage)
-        val SystemMediaControlCompatibilityChange =
-            booleanPreferencesKey(PreferKey.systemMediaControlCompatibilityChange)
-        val StreamReadAloudAudio = booleanPreferencesKey(PreferKey.streamReadAloudAudio)
-        val TtsTimer = androidx.datastore.preferences.core.intPreferencesKey(PreferKey.ttsTimer)
-        val TtsFollowSys = booleanPreferencesKey(PreferKey.ttsFollowSys)
-        val TtsSpeechRate = androidx.datastore.preferences.core.intPreferencesKey(
-            PreferKey.ttsSpeechRate
-        )
-        val SpeechAnalysisMode = stringPreferencesKey(PreferKey.speechAnalysisMode)
-        val UseMultiSpeaker = booleanPreferencesKey(PreferKey.useMultiSpeaker)
-        val DefaultInterface = stringPreferencesKey(PreferKey.defaultReadAloudInterface)
-        val ContentSelectSpeakMode = androidx.datastore.preferences.core.intPreferencesKey(
-            PreferKey.contentSelectSpeakMod
-        )
-        val AudioPreDownloadNum = androidx.datastore.preferences.core.intPreferencesKey(
-            PreferKey.audioPreDownloadNum
-        )
-        val PreSynthesisConcurrency = androidx.datastore.preferences.core.intPreferencesKey(
-            PreferKey.ttsPreSynthesisConcurrency
+    override suspend fun update(transform: (ReadAloudSettings) -> ReadAloudSettings) {
+        AppConfigStore.atomicUpdate(
+            read = Preferences::toReadAloudSettings,
+            toPrefMap = ReadAloudSettings::toPrefMap,
+            transform = transform,
         )
     }
 
     companion object {
-        const val KEY_MEDIA_BUTTON_PER_NEXT = "mediaButtonPerNext"
         const val DEFAULT_INTERFACE_CLASSIC = "classic"
         const val DEFAULT_INTERFACE_PLAYER = "player"
         val AVAILABLE_INTERFACES = setOf(DEFAULT_INTERFACE_CLASSIC, DEFAULT_INTERFACE_PLAYER)
     }
 }
+
+internal fun Preferences.toReadAloudSettings(): ReadAloudSettings = ReadAloudSettings(
+    ttsEngine = compatDsString(PreferKey.ttsEngine),
+    ttsParagraphInterval = compatDsValue(ReadAloudKeys.TtsParagraphInterval, 0),
+    audioCacheCleanTime = compatDsValue(ReadAloudKeys.AudioCacheCleanTime, 10),
+    ignoreAudioFocus = compatDsValue(ReadAloudKeys.IgnoreAudioFocus, false),
+    mediaButtonOnExit = compatDsValue(ReadAloudKeys.MediaButtonOnExit, true),
+    readAloudByMediaButton = compatDsValue(ReadAloudKeys.ReadAloudByMediaButton, false),
+    pauseReadAloudWhilePhoneCalls =
+        compatDsValue(ReadAloudKeys.PauseReadAloudWhilePhoneCalls, false),
+    readAloudWakeLock = compatDsValue(ReadAloudKeys.ReadAloudWakeLock, false),
+    showReadAloudCapsule = compatDsValue(ReadAloudKeys.ShowReadAloudCapsule, true),
+    capsuleAutoCollapse = compatDsValue(ReadAloudKeys.CapsuleAutoCollapse, true),
+    capsuleOffsetX = compatDsValue(ReadAloudKeys.CapsuleOffsetX, 0f),
+    capsuleOffsetY = compatDsValue(ReadAloudKeys.CapsuleOffsetY, 0f),
+    mediaButtonPerNext = compatDsValue(ReadAloudKeys.MediaButtonPerNext, false),
+    readAloudByPage = compatDsValue(ReadAloudKeys.ReadAloudByPage, false),
+    systemMediaControlCompatibilityChange =
+        compatDsValue(ReadAloudKeys.SystemMediaControlCompatibilityChange, true),
+    streamReadAloudAudio = compatDsValue(ReadAloudKeys.StreamReadAloudAudio, false),
+    ttsTimer = PlaybackTimer.normalize(compatDsValue(ReadAloudKeys.TtsTimer, 0)),
+    ttsFollowSys = compatDsValue(ReadAloudKeys.TtsFollowSys, true),
+    ttsSpeechRate = compatDsValue(ReadAloudKeys.TtsSpeechRate, 5),
+    speechAnalysisMode = compatDsValue(ReadAloudKeys.SpeechAnalysisMode, "rule"),
+    useMultiSpeaker = compatDsValue(ReadAloudKeys.UseMultiSpeaker, true),
+    defaultInterface = compatDsValue(
+        ReadAloudKeys.DefaultInterface,
+        ReadAloudSettingsRepository.DEFAULT_INTERFACE_CLASSIC,
+    ),
+    contentSelectSpeakMode = compatDsValue(ReadAloudKeys.ContentSelectSpeakMode, 0),
+    audioPreDownloadNum = compatDsValue(ReadAloudKeys.AudioPreDownloadNum, 10),
+    ttsPreSynthesisConcurrency = compatDsValue(ReadAloudKeys.PreSynthesisConcurrency, 3),
+)
+
+internal fun ReadAloudSettings.toPrefMap(): Map<String, Any?> = mapOf(
+    PreferKey.ttsEngine to ttsEngine,
+    PreferKey.ttsParagraphInterval to ttsParagraphInterval,
+    PreferKey.audioCacheCleanTime to audioCacheCleanTime,
+    PreferKey.ignoreAudioFocus to ignoreAudioFocus,
+    PreferKey.mediaButtonOnExit to mediaButtonOnExit,
+    PreferKey.readAloudByMediaButton to readAloudByMediaButton,
+    PreferKey.pauseReadAloudWhilePhoneCalls to pauseReadAloudWhilePhoneCalls,
+    PreferKey.readAloudWakeLock to readAloudWakeLock,
+    PreferKey.showReadAloudCapsule to showReadAloudCapsule,
+    PreferKey.capsuleAutoCollapse to capsuleAutoCollapse,
+    ReadAloudKeys.CapsuleOffsetX.name to capsuleOffsetX,
+    ReadAloudKeys.CapsuleOffsetY.name to capsuleOffsetY,
+    MEDIA_BUTTON_PER_NEXT to mediaButtonPerNext,
+    PreferKey.readAloudByPage to readAloudByPage,
+    PreferKey.systemMediaControlCompatibilityChange to systemMediaControlCompatibilityChange,
+    PreferKey.streamReadAloudAudio to streamReadAloudAudio,
+    PreferKey.ttsTimer to ttsTimer,
+    PreferKey.ttsFollowSys to ttsFollowSys,
+    PreferKey.ttsSpeechRate to ttsSpeechRate,
+    PreferKey.speechAnalysisMode to speechAnalysisMode,
+    PreferKey.useMultiSpeaker to useMultiSpeaker,
+    PreferKey.defaultReadAloudInterface to defaultInterface,
+    PreferKey.contentSelectSpeakMod to contentSelectSpeakMode,
+    PreferKey.audioPreDownloadNum to audioPreDownloadNum,
+    PreferKey.ttsPreSynthesisConcurrency to ttsPreSynthesisConcurrency,
+)
+
+private const val MEDIA_BUTTON_PER_NEXT = "mediaButtonPerNext"
+
+private object ReadAloudKeys {
+    val TtsParagraphInterval = intPreferencesKey(PreferKey.ttsParagraphInterval)
+    val AudioCacheCleanTime = intPreferencesKey(PreferKey.audioCacheCleanTime)
+    val IgnoreAudioFocus = booleanPreferencesKey(PreferKey.ignoreAudioFocus)
+    val MediaButtonOnExit = booleanPreferencesKey(PreferKey.mediaButtonOnExit)
+    val ReadAloudByMediaButton = booleanPreferencesKey(PreferKey.readAloudByMediaButton)
+    val PauseReadAloudWhilePhoneCalls =
+        booleanPreferencesKey(PreferKey.pauseReadAloudWhilePhoneCalls)
+    val ReadAloudWakeLock = booleanPreferencesKey(PreferKey.readAloudWakeLock)
+    val ShowReadAloudCapsule = booleanPreferencesKey(PreferKey.showReadAloudCapsule)
+    val CapsuleAutoCollapse = booleanPreferencesKey(PreferKey.capsuleAutoCollapse)
+    val CapsuleOffsetX = floatPreferencesKey("read_aloud_capsule_offset_x")
+    val CapsuleOffsetY = floatPreferencesKey("read_aloud_capsule_offset_y")
+    val MediaButtonPerNext = booleanPreferencesKey(MEDIA_BUTTON_PER_NEXT)
+    val ReadAloudByPage = booleanPreferencesKey(PreferKey.readAloudByPage)
+    val SystemMediaControlCompatibilityChange =
+        booleanPreferencesKey(PreferKey.systemMediaControlCompatibilityChange)
+    val StreamReadAloudAudio = booleanPreferencesKey(PreferKey.streamReadAloudAudio)
+    val TtsTimer = intPreferencesKey(PreferKey.ttsTimer)
+    val TtsFollowSys = booleanPreferencesKey(PreferKey.ttsFollowSys)
+    val TtsSpeechRate = intPreferencesKey(PreferKey.ttsSpeechRate)
+    val SpeechAnalysisMode = stringPreferencesKey(PreferKey.speechAnalysisMode)
+    val UseMultiSpeaker = booleanPreferencesKey(PreferKey.useMultiSpeaker)
+    val DefaultInterface = stringPreferencesKey(PreferKey.defaultReadAloudInterface)
+    val ContentSelectSpeakMode = intPreferencesKey(PreferKey.contentSelectSpeakMod)
+    val AudioPreDownloadNum = intPreferencesKey(PreferKey.audioPreDownloadNum)
+    val PreSynthesisConcurrency = intPreferencesKey(PreferKey.ttsPreSynthesisConcurrency)
+}

+ 1 - 23
app/src/main/java/io/legado/app/domain/gateway/ReadAloudSettingsGateway.kt

@@ -6,27 +6,5 @@ import kotlinx.coroutines.flow.Flow
 interface ReadAloudSettingsGateway {
     val currentSettings: ReadAloudSettings
     val settings: Flow<ReadAloudSettings>
-    suspend fun update(update: ReadAloudSettingsUpdate)
-}
-
-sealed interface ReadAloudSettingsUpdate {
-    data class TtsEngine(val value: String?) : ReadAloudSettingsUpdate
-    data class ParagraphInterval(val value: Int) : ReadAloudSettingsUpdate
-    data class AudioCacheCleanTime(val value: Int) : ReadAloudSettingsUpdate
-    data class IgnoreAudioFocus(val value: Boolean) : ReadAloudSettingsUpdate
-    data class MediaButtonOnExit(val value: Boolean) : ReadAloudSettingsUpdate
-    data class ReadAloudByMediaButton(val value: Boolean) : ReadAloudSettingsUpdate
-    data class PauseWhilePhoneCalls(val value: Boolean) : ReadAloudSettingsUpdate
-    data class ReadAloudWakeLock(val value: Boolean) : ReadAloudSettingsUpdate
-    data class MediaButtonPerNext(val value: Boolean) : ReadAloudSettingsUpdate
-    data class ReadAloudByPage(val value: Boolean) : ReadAloudSettingsUpdate
-    data class SystemMediaControlCompatibility(val value: Boolean) : ReadAloudSettingsUpdate
-    data class StreamAudio(val value: Boolean) : ReadAloudSettingsUpdate
-    data class Timer(val value: Int) : ReadAloudSettingsUpdate
-    data class FollowSystem(val value: Boolean) : ReadAloudSettingsUpdate
-    data class SpeechRate(val value: Int) : ReadAloudSettingsUpdate
-    data class ContentSelectSpeakMode(val value: Int) : ReadAloudSettingsUpdate
-    data class AudioPreDownloadNum(val value: Int) : ReadAloudSettingsUpdate
-    data class PreSynthesisConcurrency(val value: Int) : ReadAloudSettingsUpdate
-    data class CapsuleAutoCollapse(val value: Boolean) : ReadAloudSettingsUpdate
+    suspend fun update(transform: (ReadAloudSettings) -> ReadAloudSettings)
 }

+ 5 - 5
app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt

@@ -47,7 +47,6 @@ import io.legado.app.domain.model.PlaybackTimer
 import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
-import io.legado.app.domain.gateway.ReadAloudSettingsUpdate
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.book.removeType
 import io.legado.app.help.config.AppConfig
@@ -208,11 +207,12 @@ class AudioPlayActivity :
                 }
             }
             R.id.menu_media_control -> lifecycleScope.launch {
-                readAloudSettingsGateway.update(
-                    ReadAloudSettingsUpdate.SystemMediaControlCompatibility(
-                        !readAloudSettingsGateway.currentSettings.systemMediaControlCompatibilityChange
+                readAloudSettingsGateway.update {
+                    it.copy(
+                        systemMediaControlCompatibilityChange =
+                            !it.systemMediaControlCompatibilityChange
                     )
-                )
+                }
             }
             R.id.menu_wake_lock -> lifecycleScope.launch {
                 otherSettingsGateway.update(

+ 70 - 22
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -999,7 +999,7 @@ class ReadBookViewModel(
             is ReadBookIntent.ApplySpeakEngine -> {
                 ReadBook.book?.setTtsEngine(null)
                 viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) {
-                    readAloudSettingsRepository.setTtsEngine(intent.value)
+                    readAloudSettingsRepository.update { it.copy(ttsEngine = intent.value) }
                     ReadAloud.upReadAloudClass()
                     _uiState.update {
                         it.copy(
@@ -1025,7 +1025,9 @@ class ReadBookViewModel(
 
             is ReadBookIntent.ApplyPreSynthesisConcurrency -> {
                 viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) {
-                    readAloudSettingsRepository.setPreSynthesisConcurrency(intent.value)
+                    readAloudSettingsRepository.update {
+                        it.copy(ttsPreSynthesisConcurrency = intent.value.coerceIn(1, 8))
+                    }
                 }
                 _uiState.update {
                     it.copy(
@@ -1037,7 +1039,9 @@ class ReadBookViewModel(
 
             is ReadBookIntent.ApplyAudioCacheCleanTime -> {
                 viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) {
-                    readAloudSettingsRepository.setAudioCacheCleanTime(intent.value)
+                    readAloudSettingsRepository.update {
+                        it.copy(audioCacheCleanTime = intent.value)
+                    }
                 }
                 _uiState.update {
                     it.copy(
@@ -1049,7 +1053,9 @@ class ReadBookViewModel(
 
             is ReadBookIntent.ApplyParagraphInterval -> {
                 viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) {
-                    readAloudSettingsRepository.setTtsParagraphInterval(intent.value)
+                    readAloudSettingsRepository.update {
+                        it.copy(ttsParagraphInterval = intent.value)
+                    }
                 }
                 _uiState.update {
                     it.copy(
@@ -1089,7 +1095,7 @@ class ReadBookViewModel(
                     loadTtsEngineItems()
                     if (ReadAloud.ttsEngine == intent.engineId.toString()) {
                         viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) {
-                            readAloudSettingsRepository.setTtsEngine(null)
+                            readAloudSettingsRepository.update { it.copy(ttsEngine = null) }
                             ReadAloud.upReadAloudClass()
                         }
                     }
@@ -1194,19 +1200,31 @@ class ReadBookViewModel(
             ReadBookIntent.SaveImportedHttpTts -> saveImportedHttpTts()
 
             is ReadBookIntent.SetReadAloudIgnoreAudioFocus -> {
-                viewModelScope.launch { readAloudSettingsRepository.setIgnoreAudioFocus(intent.value) }
+                viewModelScope.launch {
+                    readAloudSettingsRepository.update { it.copy(ignoreAudioFocus = intent.value) }
+                }
             }
             is ReadBookIntent.SetReadAloudPauseOnPhoneCall -> {
-                viewModelScope.launch { readAloudSettingsRepository.setPauseReadAloudWhilePhoneCalls(intent.value) }
+                viewModelScope.launch {
+                    readAloudSettingsRepository.update {
+                        it.copy(pauseReadAloudWhilePhoneCalls = intent.value)
+                    }
+                }
             }
             is ReadBookIntent.SetReadAloudWakeLock -> {
-                viewModelScope.launch { readAloudSettingsRepository.setReadAloudWakeLock(intent.value) }
+                viewModelScope.launch {
+                    readAloudSettingsRepository.update { it.copy(readAloudWakeLock = intent.value) }
+                }
             }
             is ReadBookIntent.SetShowReadAloudCapsule -> {
-                viewModelScope.launch { readAloudSettingsRepository.setShowReadAloudCapsule(intent.value) }
+                viewModelScope.launch {
+                    readAloudSettingsRepository.update { it.copy(showReadAloudCapsule = intent.value) }
+                }
             }
             is ReadBookIntent.SetCapsuleAutoCollapse -> {
-                viewModelScope.launch { readAloudSettingsRepository.setCapsuleAutoCollapse(intent.value) }
+                viewModelScope.launch {
+                    readAloudSettingsRepository.update { it.copy(capsuleAutoCollapse = intent.value) }
+                }
             }
             ReadBookIntent.ResetReadAloudCapsulePosition -> {
                 _uiState.update { it.copy(
@@ -1214,7 +1232,9 @@ class ReadBookViewModel(
                     readAloudCapsuleOffsetY = 0f,
                 ) }
                 viewModelScope.launch {
-                    readAloudSettingsRepository.resetCapsulePosition()
+                    readAloudSettingsRepository.update {
+                        it.copy(capsuleOffsetX = 0f, capsuleOffsetY = 0f)
+                    }
                 }
             }
             is ReadBookIntent.SetReadAloudCapsulePosition -> {
@@ -1223,21 +1243,33 @@ class ReadBookViewModel(
                     readAloudCapsuleOffsetY = intent.y,
                 ) }
                 viewModelScope.launch {
-                    readAloudSettingsRepository.setCapsulePosition(intent.x, intent.y)
+                    readAloudSettingsRepository.update {
+                        it.copy(capsuleOffsetX = intent.x, capsuleOffsetY = intent.y)
+                    }
                 }
             }
             is ReadBookIntent.SetReadAloudMediaButtonPerNext -> {
-                viewModelScope.launch { readAloudSettingsRepository.setMediaButtonPerNext(intent.value) }
+                viewModelScope.launch {
+                    readAloudSettingsRepository.update { it.copy(mediaButtonPerNext = intent.value) }
+                }
             }
             is ReadBookIntent.SetReadAloudByPage -> {
-                viewModelScope.launch { readAloudSettingsRepository.setReadAloudByPage(intent.value) }
+                viewModelScope.launch {
+                    readAloudSettingsRepository.update { it.copy(readAloudByPage = intent.value) }
+                }
                 if (intent.value) postEvent(EventBus.MEDIA_BUTTON, false)
             }
             is ReadBookIntent.SetReadAloudSystemMediaCompat -> {
-                viewModelScope.launch { readAloudSettingsRepository.setSystemMediaControlCompatibilityChange(intent.value) }
+                viewModelScope.launch {
+                    readAloudSettingsRepository.update {
+                        it.copy(systemMediaControlCompatibilityChange = intent.value)
+                    }
+                }
             }
             is ReadBookIntent.SetReadAloudStreamAudio -> {
-                viewModelScope.launch { readAloudSettingsRepository.setStreamReadAloudAudio(intent.value) }
+                viewModelScope.launch {
+                    readAloudSettingsRepository.update { it.copy(streamReadAloudAudio = intent.value) }
+                }
                 if (intent.value) postEvent(EventBus.MEDIA_BUTTON, false)
             }
             is ReadBookIntent.ReadAloudPrevParagraph -> ReadAloud.prevParagraph(context)
@@ -1254,7 +1286,9 @@ class ReadBookViewModel(
             is ReadBookIntent.ReadAloudNextChapter -> ReadBook.moveToNextChapter(true)
             is ReadBookIntent.SetReadAloudTtsTimer -> setReadAloudTtsTimer(intent.value)
             is ReadBookIntent.SetReadAloudTtsFollowSys -> {
-                viewModelScope.launch { readAloudSettingsRepository.setTtsFollowSys(intent.value) }
+                viewModelScope.launch {
+                    readAloudSettingsRepository.update { it.copy(ttsFollowSys = intent.value) }
+                }
                 _uiState.update { it.copy(readAloudTtsFollowSys = intent.value) }
             }
             is ReadBookIntent.SetReadAloudTtsSpeechRate -> setReadAloudTtsSpeechRate(intent.value)
@@ -1270,7 +1304,9 @@ class ReadBookViewModel(
                             return@launch
                         }
                     }
-                    readAloudSettingsRepository.setSpeechAnalysisMode(intent.value)
+                    readAloudSettingsRepository.update {
+                        it.copy(speechAnalysisMode = intent.value)
+                    }
                     _uiState.update { it.copy(speechAnalysisMode = intent.value) }
                 }
             }
@@ -1280,7 +1316,9 @@ class ReadBookViewModel(
                     val resumePlaying = shouldRestart && !BaseReadAloudService.pause
                     val chapter = ReadBook.curTextChapter
                     val chapterPosition = readAloudSessionStore.state.value.playback.chapterPosition
-                    readAloudSettingsRepository.setUseMultiSpeaker(intent.value)
+                    readAloudSettingsRepository.update {
+                        it.copy(useMultiSpeaker = intent.value)
+                    }
                     _uiState.update { it.copy(useMultiSpeaker = intent.value) }
                     if (shouldRestart && chapter != null) {
                         val pageIndex = chapter.getPageIndexByCharIndex(chapterPosition)
@@ -1304,7 +1342,13 @@ class ReadBookViewModel(
             }
             is ReadBookIntent.SetDefaultReadAloudInterface -> {
                 viewModelScope.launch {
-                    readAloudSettingsRepository.setDefaultInterface(intent.value)
+                    readAloudSettingsRepository.update {
+                        it.copy(
+                            defaultInterface = intent.value.takeIf { value ->
+                                value in ReadAloudSettingsRepository.AVAILABLE_INTERFACES
+                            } ?: ReadAloudSettingsRepository.DEFAULT_INTERFACE_CLASSIC
+                        )
+                    }
                 }
                 _uiState.update { it.copy(defaultReadAloudInterface = intent.value) }
             }
@@ -2235,13 +2279,17 @@ class ReadBookViewModel(
     private fun setReadAloudTtsTimer(value: Int) {
         val timer = PlaybackTimer.normalize(value)
         ReadAloud.setTimer(context, timer)
-        viewModelScope.launch { readAloudSettingsRepository.setTtsTimer(timer) }
+        viewModelScope.launch {
+            readAloudSettingsRepository.update { it.copy(ttsTimer = timer) }
+        }
         _uiState.update { it.copy(readAloudTtsTimer = timer) }
     }
 
     private fun setReadAloudTtsSpeechRate(value: Int) {
         viewModelScope.launch {
-            readAloudSettingsRepository.setTtsSpeechRate(value)
+            readAloudSettingsRepository.update {
+                it.copy(ttsSpeechRate = value.coerceIn(0, 80))
+            }
             ReadAloud.upTtsSpeechRate(context)
         }
         _uiState.update { it.copy(readAloudTtsSpeechRate = value) }

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

@@ -5,7 +5,6 @@ import androidx.lifecycle.Observer
 import com.jeremyliao.liveeventbus.LiveEventBus
 import io.legado.app.constant.EventBus
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
-import io.legado.app.domain.gateway.ReadAloudSettingsUpdate
 import io.legado.app.domain.model.readaloud.ReadAloudSessionStatus
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadAloudSessionStore
@@ -128,7 +127,7 @@ class ReadAloudPlayerCoordinator(
     fun nextChapter() = ReadBook.moveToNextChapter(true)
 
     suspend fun setSpeed(value: Int) {
-        readAloudSettingsGateway.update(ReadAloudSettingsUpdate.SpeechRate(value))
+        readAloudSettingsGateway.update { it.copy(ttsSpeechRate = value.coerceIn(0, 80)) }
         ReadAloud.upTtsSpeechRate(application)
     }
 

+ 8 - 6
app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt

@@ -16,8 +16,8 @@ import io.legado.app.domain.gateway.OtherConfigSystemGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
-import io.legado.app.domain.gateway.ReadAloudSettingsUpdate
 import io.legado.app.domain.model.settings.OtherSettings
+import io.legado.app.domain.model.settings.ReadAloudSettings
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -113,11 +113,11 @@ class OtherConfigViewModel(
             is OtherConfigIntent.ReplaceEnableDefaultChanged ->
                 updateOtherSetting(OtherSettingsUpdate.ReplaceEnableDefault(intent.value))
             is OtherConfigIntent.MediaButtonOnExitChanged ->
-                updateReadAloudSetting(ReadAloudSettingsUpdate.MediaButtonOnExit(intent.value))
+                updateReadAloudSetting { it.copy(mediaButtonOnExit = intent.value) }
             is OtherConfigIntent.ReadAloudByMediaButtonChanged ->
-                updateReadAloudSetting(ReadAloudSettingsUpdate.ReadAloudByMediaButton(intent.value))
+                updateReadAloudSetting { it.copy(readAloudByMediaButton = intent.value) }
             is OtherConfigIntent.IgnoreAudioFocusChanged ->
-                updateReadAloudSetting(ReadAloudSettingsUpdate.IgnoreAudioFocus(intent.value))
+                updateReadAloudSetting { it.copy(ignoreAudioFocus = intent.value) }
             is OtherConfigIntent.AutoClearExpiredChanged ->
                 updateOtherSetting(OtherSettingsUpdate.AutoClearExpired(intent.value))
             is OtherConfigIntent.ShowAddToShelfAlertChanged ->
@@ -236,9 +236,11 @@ class OtherConfigViewModel(
         }
     }
 
-    private fun updateReadAloudSetting(update: ReadAloudSettingsUpdate) {
+    private fun updateReadAloudSetting(
+        transform: (ReadAloudSettings) -> ReadAloudSettings,
+    ) {
         viewModelScope.launch {
-            runCatching { readAloudSettingsGateway.update(update) }
+            runCatching { readAloudSettingsGateway.update(transform) }
                 .onFailure { showMessage(it.localizedMessage ?: "设置失败") }
         }
     }

+ 113 - 0
app/src/test/java/io/legado/app/data/repository/ReadAloudSettingsMappingTest.kt

@@ -0,0 +1,113 @@
+package io.legado.app.data.repository
+
+import io.legado.app.constant.PreferKey
+import io.legado.app.domain.model.settings.ReadAloudSettings
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ReadAloudSettingsMappingTest {
+
+    @Test
+    fun `朗读设置 23 键写映射逐字段对应`() {
+        readAloudMappingSamples().forEach { settings ->
+            assertEquals(settings.expectedPrefMap(), settings.toPrefMap())
+        }
+    }
+
+    @Test
+    fun `朗读设置 23 键读映射逐字段对应`() {
+        readAloudMappingSamples().forEach { expected ->
+            assertEquals(expected, expected.expectedPrefMap().toTestPreferences().toReadAloudSettings())
+        }
+    }
+
+    @Test
+    fun `胶囊坐标与 nullable 引擎通过真实原子路径单批写入`() {
+        val values = captureAtomicUpdateValues(
+            current = ReadAloudSettings(ttsEngine = "engine-old"),
+            read = { it.toReadAloudSettings() },
+            toPrefMap = ReadAloudSettings::toPrefMap,
+            transform = {
+                it.copy(
+                    ttsEngine = null,
+                    capsuleOffsetX = 12.5f,
+                    capsuleOffsetY = -8.25f,
+                )
+            },
+        )
+
+        assertEquals(
+            mapOf(
+                PreferKey.ttsEngine to null,
+                CAPSULE_OFFSET_X to 12.5f,
+                CAPSULE_OFFSET_Y to -8.25f,
+            ),
+            values,
+        )
+    }
+}
+
+private const val CAPSULE_OFFSET_X = "read_aloud_capsule_offset_x"
+private const val CAPSULE_OFFSET_Y = "read_aloud_capsule_offset_y"
+private const val MEDIA_BUTTON_PER_NEXT = "mediaButtonPerNext"
+
+private fun readAloudMappingSamples(): List<ReadAloudSettings> {
+    val base = ReadAloudSettings(
+        ttsEngine = "engine-unique",
+        ttsParagraphInterval = 11,
+        audioCacheCleanTime = 22,
+        capsuleOffsetX = 3.25f,
+        capsuleOffsetY = -4.5f,
+        ttsTimer = 33,
+        ttsSpeechRate = 44,
+        speechAnalysisMode = "ai",
+        defaultInterface = "player",
+        contentSelectSpeakMode = 55,
+        audioPreDownloadNum = 66,
+        capsuleAutoCollapse = false,
+        ttsPreSynthesisConcurrency = 7,
+    )
+    return listOf(
+        base,
+        base.copy(ignoreAudioFocus = true),
+        base.copy(mediaButtonOnExit = false),
+        base.copy(readAloudByMediaButton = true),
+        base.copy(pauseReadAloudWhilePhoneCalls = true),
+        base.copy(readAloudWakeLock = true),
+        base.copy(showReadAloudCapsule = false),
+        base.copy(mediaButtonPerNext = true),
+        base.copy(readAloudByPage = true),
+        base.copy(systemMediaControlCompatibilityChange = false),
+        base.copy(streamReadAloudAudio = true),
+        base.copy(ttsFollowSys = false),
+        base.copy(useMultiSpeaker = false),
+    )
+}
+
+private fun ReadAloudSettings.expectedPrefMap(): Map<String, Any?> = mapOf(
+    PreferKey.ttsEngine to ttsEngine,
+    PreferKey.ttsParagraphInterval to ttsParagraphInterval,
+    PreferKey.audioCacheCleanTime to audioCacheCleanTime,
+    PreferKey.ignoreAudioFocus to ignoreAudioFocus,
+    PreferKey.mediaButtonOnExit to mediaButtonOnExit,
+    PreferKey.readAloudByMediaButton to readAloudByMediaButton,
+    PreferKey.pauseReadAloudWhilePhoneCalls to pauseReadAloudWhilePhoneCalls,
+    PreferKey.readAloudWakeLock to readAloudWakeLock,
+    PreferKey.showReadAloudCapsule to showReadAloudCapsule,
+    PreferKey.capsuleAutoCollapse to capsuleAutoCollapse,
+    CAPSULE_OFFSET_X to capsuleOffsetX,
+    CAPSULE_OFFSET_Y to capsuleOffsetY,
+    MEDIA_BUTTON_PER_NEXT to mediaButtonPerNext,
+    PreferKey.readAloudByPage to readAloudByPage,
+    PreferKey.systemMediaControlCompatibilityChange to systemMediaControlCompatibilityChange,
+    PreferKey.streamReadAloudAudio to streamReadAloudAudio,
+    PreferKey.ttsTimer to ttsTimer,
+    PreferKey.ttsFollowSys to ttsFollowSys,
+    PreferKey.ttsSpeechRate to ttsSpeechRate,
+    PreferKey.speechAnalysisMode to speechAnalysisMode,
+    PreferKey.useMultiSpeaker to useMultiSpeaker,
+    PreferKey.defaultReadAloudInterface to defaultInterface,
+    PreferKey.contentSelectSpeakMod to contentSelectSpeakMode,
+    PreferKey.audioPreDownloadNum to audioPreDownloadNum,
+    PreferKey.ttsPreSynthesisConcurrency to ttsPreSynthesisConcurrency,
+)

+ 5 - 2
app/src/test/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModelTest.kt

@@ -14,7 +14,6 @@ import io.legado.app.domain.gateway.OtherConfigSystemGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
-import io.legado.app.domain.gateway.ReadAloudSettingsUpdate
 import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.domain.model.settings.ReadAloudSettings
 import io.legado.app.domain.model.settings.DownloadCacheSettings
@@ -212,7 +211,11 @@ class OtherConfigViewModelTest {
             get() = state.value
         override val settings: Flow<ReadAloudSettings> = state
 
-        override suspend fun update(update: ReadAloudSettingsUpdate) = Unit
+        override suspend fun update(
+            transform: (ReadAloudSettings) -> ReadAloudSettings,
+        ) {
+            state.value = transform(state.value)
+        }
     }
 
     private class FakeDownloadCacheSettingsGateway : DownloadCacheSettingsGateway {