Răsfoiți Sursa

feat: 新版听书界面动态流光家人们

HapeLee 3 luni în urmă
părinte
comite
a3e2c4484c

+ 7 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -163,6 +163,7 @@ object PreferKey {
     const val showReadAloudCapsule = "showReadAloudCapsule"
     const val showReadAloudCapsule = "showReadAloudCapsule"
     const val capsuleAutoCollapse = "capsuleAutoCollapse"
     const val capsuleAutoCollapse = "capsuleAutoCollapse"
     const val defaultReadAloudInterface = "defaultReadAloudInterface"
     const val defaultReadAloudInterface = "defaultReadAloudInterface"
+    const val readAloudPlayerBgMode = "readAloudPlayerBgMode"
     const val showLastUpdateTime = "showLastUpdateTime"
     const val showLastUpdateTime = "showLastUpdateTime"
     const val showWaitUpCount = "showWaitUpCount"
     const val showWaitUpCount = "showWaitUpCount"
     const val clearWebViewData = "clearWebViewData"
     const val clearWebViewData = "clearWebViewData"
@@ -425,3 +426,9 @@ object ReadMenuBlurStyle {
     const val Solid = 0
     const val Solid = 0
     const val Progressive = 1
     const val Progressive = 1
 }
 }
+
+object ReadAloudBgMode {
+    const val Solid = 0
+    const val Blur = 1
+    const val FlowingLight = 2
+}

+ 1 - 1
app/src/main/java/io/legado/app/data/repository/ReadSettingsRepository.kt

@@ -386,7 +386,7 @@ class ReadSettingsRepository(
             showBrightnessView = compatDsValue(Keys.ShowBrightnessView, "0"),
             showBrightnessView = compatDsValue(Keys.ShowBrightnessView, "0"),
             brightnessVwPos = compatDsValue(Keys.BrightnessVwPos, "1"),
             brightnessVwPos = compatDsValue(Keys.BrightnessVwPos, "1"),
             readBrightness = compatDsValue(Keys.ReadBrightness, 100),
             readBrightness = compatDsValue(Keys.ReadBrightness, 100),
-            brightnessAuto = compatDsValue(Keys.BrightnessAuto, false),
+            brightnessAuto = compatDsValue(Keys.BrightnessAuto, true),
             useUnderline = compatDsValue(Keys.UseUnderline, false),
             useUnderline = compatDsValue(Keys.UseUnderline, false),
             readSliderMode = compatDsValue(Keys.ReadSliderMode, "0"),
             readSliderMode = compatDsValue(Keys.ReadSliderMode, "0"),
             doubleHorizontalPage = compatDsValue(Keys.DoubleHorizontalPage, "0"),
             doubleHorizontalPage = compatDsValue(Keys.DoubleHorizontalPage, "0"),

+ 1 - 1
app/src/main/java/io/legado/app/domain/model/settings/ReadSettings.kt

@@ -20,7 +20,7 @@ data class ReadSettings(
     val showBrightnessView: String = "0",
     val showBrightnessView: String = "0",
     val brightnessVwPos: String = "1",
     val brightnessVwPos: String = "1",
     val readBrightness: Int = 100,
     val readBrightness: Int = 100,
-    val brightnessAuto: Boolean = false,
+    val brightnessAuto: Boolean = true,
     val useUnderline: Boolean = false,
     val useUnderline: Boolean = false,
     val readSliderMode: String = "0",
     val readSliderMode: String = "0",
     val doubleHorizontalPage: String = "0",
     val doubleHorizontalPage: String = "0",

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

@@ -401,7 +401,7 @@ data class ReadMenuConfig(
     val showBrightnessView: String = "0",
     val showBrightnessView: String = "0",
     val brightnessVwPos: String = "1",
     val brightnessVwPos: String = "1",
     val readBrightness: Int = 100,
     val readBrightness: Int = 100,
-    val brightnessAuto: Boolean = false,
+    val brightnessAuto: Boolean = true,
     val showMenuIcon: Boolean = false,
     val showMenuIcon: Boolean = false,
     val titleBarCompact: Boolean = false,
     val titleBarCompact: Boolean = false,
 )
 )

+ 0 - 2
app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleConfigSheet.kt

@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.lazy.rememberLazyListState
@@ -260,7 +259,6 @@ private fun HighlightRuleItem(
                 TinySwitch(
                 TinySwitch(
                     checked = rule.enabled,
                     checked = rule.enabled,
                     onCheckedChange = onToggle,
                     onCheckedChange = onToggle,
-                    modifier = Modifier.size(36.dp),
                 )
                 )
                 SmallTonalButton(
                 SmallTonalButton(
                     onClick = onEditClick,
                     onClick = onEditClick,

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

@@ -31,6 +31,7 @@ data class ReadAloudPlayerUiState(
     val isPaused: Boolean = false,
     val isPaused: Boolean = false,
     val speed: Int = 10,
     val speed: Int = 10,
     val timerMinutes: Int = 0,
     val timerMinutes: Int = 0,
+    val bgMode: Int = 0,
 )
 )
 
 
 sealed interface ReadAloudPlayerIntent {
 sealed interface ReadAloudPlayerIntent {
@@ -43,6 +44,7 @@ sealed interface ReadAloudPlayerIntent {
     data object OpenSettings : ReadAloudPlayerIntent
     data object OpenSettings : ReadAloudPlayerIntent
     data object SwitchToClassic : ReadAloudPlayerIntent
     data object SwitchToClassic : ReadAloudPlayerIntent
     data object OpenToc : ReadAloudPlayerIntent
     data object OpenToc : ReadAloudPlayerIntent
+    data object CycleBgMode : ReadAloudPlayerIntent
     data class SetSpeed(val value: Int) : ReadAloudPlayerIntent
     data class SetSpeed(val value: Int) : ReadAloudPlayerIntent
     data class SetTimer(val minutes: Int) : ReadAloudPlayerIntent
     data class SetTimer(val minutes: Int) : ReadAloudPlayerIntent
     data class SeekTo(val chapterPosition: Int) : ReadAloudPlayerIntent
     data class SeekTo(val chapterPosition: Int) : ReadAloudPlayerIntent

+ 178 - 5
app/src/main/java/io/legado/app/ui/book/readaloud/player/ReadAloudPlayerScreen.kt

@@ -48,6 +48,7 @@ import androidx.compose.material.icons.filled.Settings
 import androidx.compose.material.icons.filled.Speed
 import androidx.compose.material.icons.filled.Speed
 import androidx.compose.material.icons.filled.Timer
 import androidx.compose.material.icons.filled.Timer
 import androidx.compose.material.icons.filled.Tune
 import androidx.compose.material.icons.filled.Tune
+import androidx.compose.material.icons.filled.WbTwilight
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ModalBottomSheet
 import androidx.compose.material3.ModalBottomSheet
 import androidx.compose.material3.SheetValue.Expanded
 import androidx.compose.material3.SheetValue.Expanded
@@ -95,20 +96,31 @@ import dev.chrisbanes.haze.hazeEffect
 import dev.chrisbanes.haze.hazeSource
 import dev.chrisbanes.haze.hazeSource
 import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
 import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
 import io.legado.app.R
 import io.legado.app.R
+import io.legado.app.constant.ReadAloudBgMode
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.hazeStyle.HazeLegado
 import io.legado.app.ui.theme.hazeStyle.HazeLegado
+import io.legado.app.ui.util.rememberBlurBackdrop
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.button.series.MediumTonalButton
 import io.legado.app.ui.widget.components.button.series.MediumTonalButton
 import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.card.TextCard
+import io.legado.app.ui.widget.components.effect.BgEffectBackground
+import io.legado.app.ui.widget.components.effect.BgEffectConfig
 import io.legado.app.ui.widget.components.image.cover.BookCoverImage
 import io.legado.app.ui.widget.components.image.cover.BookCoverImage
 import io.legado.app.ui.widget.components.image.cover.CoverBlurBackdrop
 import io.legado.app.ui.widget.components.image.cover.CoverBlurBackdrop
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
+import top.yukonga.miuix.kmp.blur.BlendColorEntry
+import top.yukonga.miuix.kmp.blur.BlurBlendMode
+import top.yukonga.miuix.kmp.blur.BlurColors
+import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported
+import top.yukonga.miuix.kmp.blur.layerBackdrop
+import top.yukonga.miuix.kmp.blur.textureBlur
 import kotlin.math.abs
 import kotlin.math.abs
 import kotlin.math.roundToInt
 import kotlin.math.roundToInt
+import androidx.compose.ui.graphics.BlendMode as ComposeBlendMode
 
 
 @Composable
 @Composable
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
@@ -161,6 +173,19 @@ fun ReadAloudPlayerScreenContent(
     }
     }
     var isTextPageUserScrolling by remember { mutableStateOf(false) }
     var isTextPageUserScrolling by remember { mutableStateOf(false) }
     val pagerHazeState = remember { HazeState() }
     val pagerHazeState = remember { HazeState() }
+    val textBackdrop = rememberBlurBackdrop()
+    val flowingLightActive = state.bgMode == ReadAloudBgMode.FlowingLight
+    val flowingTextModifier = if (flowingLightActive && textBackdrop != null) {
+        Modifier.textureBlur(
+            backdrop = textBackdrop,
+            shape = RoundedCornerShape(4.dp),
+            blurRadius = 150f,
+            colors = BlurColors(blendColors = flowingTextBlend()),
+            contentBlendMode = ComposeBlendMode.DstIn,
+        )
+    } else {
+        Modifier
+    }
     LaunchedEffect(pagerState.currentPage) {
     LaunchedEffect(pagerState.currentPage) {
         if (pagerState.currentPage != 1) isTextPageUserScrolling = false
         if (pagerState.currentPage != 1) isTextPageUserScrolling = false
     }
     }
@@ -310,6 +335,12 @@ fun ReadAloudPlayerScreenContent(
                             icon = Icons.Default.Tune,
                             icon = Icons.Default.Tune,
                             contentDescription = stringResource(R.string.switch_to_classic_read_aloud),
                             contentDescription = stringResource(R.string.switch_to_classic_read_aloud),
                         )
                         )
+                        SmallPlainButton(
+                            onClick = { onIntent(ReadAloudPlayerIntent.CycleBgMode) },
+                            icon = Icons.Default.WbTwilight,
+                            contentDescription = bgModeLabel(state.bgMode),
+                            selected = state.bgMode == ReadAloudBgMode.FlowingLight,
+                        )
                         SmallPlainButton(
                         SmallPlainButton(
                             onClick = {
                             onClick = {
                                 activeAdjustment = activeAdjustment.toggle(PlayerAdjustment.Speed)
                                 activeAdjustment = activeAdjustment.toggle(PlayerAdjustment.Speed)
@@ -362,18 +393,26 @@ fun ReadAloudPlayerScreenContent(
         Box(Modifier
         Box(Modifier
             .fillMaxSize()
             .fillMaxSize()
             .hazeSource(pagerHazeState)) {
             .hazeSource(pagerHazeState)) {
-            CoverBlurBackdrop(state.bookName, state.author, state.coverPath, state.sourceOrigin)
+            ReadAloudBackground(
+                state = state,
+                modifier = if (flowingLightActive && textBackdrop != null) {
+                    Modifier.layerBackdrop(textBackdrop)
+                } else {
+                    Modifier
+                },
+            )
             HorizontalPager(
             HorizontalPager(
                 state = pagerState,
                 state = pagerState,
                 modifier = Modifier.fillMaxSize(),
                 modifier = Modifier.fillMaxSize(),
                 verticalAlignment = Alignment.CenterVertically,
                 verticalAlignment = Alignment.CenterVertically,
             ) { page ->
             ) { page ->
                 if (page == 0) {
                 if (page == 0) {
-                    CoverPage(state, pageContentPadding)
+                    CoverPage(state, pageContentPadding, flowingTextModifier)
                 } else {
                 } else {
                     ChapterTextPage(
                     ChapterTextPage(
                         state = state,
                         state = state,
                         contentPadding = pageContentPadding,
                         contentPadding = pageContentPadding,
+                        flowingTextModifier = flowingTextModifier,
                         onIntent = onIntent,
                         onIntent = onIntent,
                         onUserScrollChanged = { isTextPageUserScrolling = it },
                         onUserScrollChanged = { isTextPageUserScrolling = it },
                     )
                     )
@@ -481,6 +520,7 @@ private fun PlayerProgressSlider(
 private fun CoverPage(
 private fun CoverPage(
     state: ReadAloudPlayerUiState,
     state: ReadAloudPlayerUiState,
     contentPadding: PaddingValues,
     contentPadding: PaddingValues,
+    flowingTextModifier: Modifier,
 ) {
 ) {
     Column(
     Column(
         modifier = Modifier
         modifier = Modifier
@@ -531,7 +571,9 @@ private fun CoverPage(
                     text = current.ifBlank {
                     text = current.ifBlank {
                         stringResource(R.string.read_aloud_preparing_content)
                         stringResource(R.string.read_aloud_preparing_content)
                     },
                     },
-                    modifier = Modifier.fillMaxWidth(),
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .then(flowingTextModifier),
                     style = LegadoTheme.typography.bodyLargeEmphasized,
                     style = LegadoTheme.typography.bodyLargeEmphasized,
                     color = LegadoTheme.colorScheme.onSurface,
                     color = LegadoTheme.colorScheme.onSurface,
                     minLines = 1,
                     minLines = 1,
@@ -545,7 +587,8 @@ private fun CoverPage(
                         text = next,
                         text = next,
                         modifier = Modifier
                         modifier = Modifier
                             .fillMaxWidth()
                             .fillMaxWidth()
-                            .padding(top = 8.dp),
+                            .padding(top = 8.dp)
+                            .then(flowingTextModifier),
                         style = LegadoTheme.typography.bodyMedium,
                         style = LegadoTheme.typography.bodyMedium,
                         color = LegadoTheme.colorScheme.onSurface.copy(alpha = 0.5f),
                         color = LegadoTheme.colorScheme.onSurface.copy(alpha = 0.5f),
                         minLines = 1,
                         minLines = 1,
@@ -571,6 +614,7 @@ private fun CoverPage(
 private fun ChapterTextPage(
 private fun ChapterTextPage(
     state: ReadAloudPlayerUiState,
     state: ReadAloudPlayerUiState,
     contentPadding: PaddingValues,
     contentPadding: PaddingValues,
+    flowingTextModifier: Modifier,
     onIntent: (ReadAloudPlayerIntent) -> Unit,
     onIntent: (ReadAloudPlayerIntent) -> Unit,
     onUserScrollChanged: (Boolean) -> Unit,
     onUserScrollChanged: (Boolean) -> Unit,
 ) {
 ) {
@@ -684,7 +728,8 @@ private fun ChapterTextPage(
                             .clip(RoundedCornerShape(16.dp))
                             .clip(RoundedCornerShape(16.dp))
                             .clickable { onIntent(ReadAloudPlayerIntent.SeekTo(line.chapterPosition)) }
                             .clickable { onIntent(ReadAloudPlayerIntent.SeekTo(line.chapterPosition)) }
                             .background(backgroundColor)
                             .background(backgroundColor)
-                            .padding(horizontal = 12.dp, vertical = 12.dp),
+                            .padding(horizontal = 12.dp, vertical = 12.dp)
+                            .then(flowingTextModifier),
                         style = if (active) LegadoTheme.typography.bodyLargeEmphasized else LegadoTheme.typography.bodyLarge,
                         style = if (active) LegadoTheme.typography.bodyLargeEmphasized else LegadoTheme.typography.bodyLarge,
                         color = if (active) LegadoTheme.colorScheme.onPrimaryContainer
                         color = if (active) LegadoTheme.colorScheme.onPrimaryContainer
                         else LegadoTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.72f),
                         else LegadoTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.72f),
@@ -709,3 +754,131 @@ private const val BOTTOM_BAR_RESTORE_DELAY_MILLIS = 650L
 
 
 private fun PlayerAdjustment?.toggle(value: PlayerAdjustment): PlayerAdjustment? =
 private fun PlayerAdjustment?.toggle(value: PlayerAdjustment): PlayerAdjustment? =
     if (this == value) null else value
     if (this == value) null else value
+
+@Composable
+private fun ReadAloudBackground(
+    state: ReadAloudPlayerUiState,
+    modifier: Modifier = Modifier,
+) {
+    when (state.bgMode) {
+        ReadAloudBgMode.Blur -> {
+            CoverBlurBackdrop(
+                state.bookName, state.author, state.coverPath, state.sourceOrigin,
+                modifier = modifier,
+            )
+        }
+
+        ReadAloudBgMode.FlowingLight -> {
+            val shaderSupported = remember { isRuntimeShaderSupported() }
+            if (shaderSupported) {
+                val coverPreset = rememberCoverDerivedPreset()
+                Box(modifier.fillMaxSize()) {
+                    CoverBlurBackdrop(
+                        state.bookName, state.author, state.coverPath, state.sourceOrigin,
+                        blurRadius = 64.dp,
+                    )
+                    BgEffectBackground(
+                        dynamicBackground = true,
+                        isOs3Effect = true,
+                        isFullSize = true,
+                        drawSurface = false,
+                        customPreset = coverPreset,
+                        modifier = Modifier.fillMaxSize(),
+                        alpha = { 0.5f },
+                    ) {
+                        Box(Modifier.fillMaxSize())
+                    }
+                }
+            } else {
+                CoverBlurBackdrop(
+                    state.bookName, state.author, state.coverPath, state.sourceOrigin,
+                    modifier = modifier,
+                )
+            }
+        }
+
+        else -> {
+            Box(
+                modifier = modifier
+                    .fillMaxSize()
+                    .background(LegadoTheme.colorScheme.surface),
+            )
+        }
+    }
+}
+
+@Composable
+private fun rememberCoverDerivedPreset(): BgEffectConfig.Config {
+    val primary = LegadoTheme.colorScheme.primary
+    val surface = LegadoTheme.colorScheme.surfaceContainerLowest
+    val tertiary = LegadoTheme.colorScheme.secondary
+    val isDark = LegadoTheme.isDark
+
+    return remember(primary, surface, tertiary) {
+        val p = primary.toShaderColor()
+        val s = surface.toShaderColor()
+        val t = tertiary.toShaderColor()
+
+        val m = floatArrayOf(
+            (p[0] + t[0]) / 2f,
+            (p[1] + t[1]) / 2f,
+            (p[2] + t[2]) / 2f,
+            1.0f,
+        )
+
+        fun stage(a: FloatArray, b: FloatArray, c: FloatArray, d: FloatArray): FloatArray =
+            floatArrayOf(
+                a[0], a[1], a[2], a[3],
+                b[0], b[1], b[2], b[3],
+                c[0], c[1], c[2], c[3],
+                d[0], d[1], d[2], d[3],
+            )
+
+        BgEffectConfig.Config(
+            points = floatArrayOf(
+                0.8f, 0.2f, 1.0f,
+                0.8f, 0.9f, 1.0f,
+                0.2f, 0.9f, 1.0f,
+                0.2f, 0.2f, 1.0f,
+            ),
+            colors1 = stage(p, t, s, m),
+            colors2 = stage(t, s, m, p),
+            colors3 = stage(s, m, p, t),
+            colorInterpPeriod = 96.0f,
+            lightOffset = if (isDark) 0.0f else 0.1f,
+            saturateOffset = if (isDark) 0.17f else 0.2f,
+            pointOffset = if (isDark) 0.4f else 0.2f,
+        )
+    }
+}
+
+private fun Color.toShaderColor(): FloatArray =
+    floatArrayOf(red, green, blue, alpha)
+
+@Composable
+private fun flowingTextBlend(): List<BlendColorEntry> {
+    val isDark = LegadoTheme.isDark
+    return remember(isDark) {
+        if (isDark) {
+            listOf(
+                BlendColorEntry(Color(0xe6a1a1a1), BlurBlendMode.ColorDodge),
+                BlendColorEntry(Color(0x4de6e6e6), BlurBlendMode.LinearLight),
+                BlendColorEntry(Color(0xff1af500), BlurBlendMode.Lab),
+            )
+        } else {
+            listOf(
+                BlendColorEntry(Color(0xcc4a4a4a), BlurBlendMode.ColorBurn),
+                BlendColorEntry(Color(0xff4f4f4f), BlurBlendMode.LinearLight),
+                BlendColorEntry(Color(0xff1af200), BlurBlendMode.Lab),
+            )
+        }
+    }
+}
+
+@Composable
+private fun bgModeLabel(mode: Int): String = when (mode) {
+    ReadAloudBgMode.Solid -> stringResource(R.string.read_aloud_bg_solid)
+    ReadAloudBgMode.Blur -> stringResource(R.string.read_aloud_bg_blur)
+    ReadAloudBgMode.FlowingLight -> stringResource(R.string.read_aloud_bg_flowing_light)
+    else -> stringResource(R.string.read_aloud_bg_blur)
+}

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

@@ -2,10 +2,14 @@ package io.legado.app.ui.book.readaloud.player
 
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import androidx.lifecycle.viewModelScope
+import io.legado.app.constant.PreferKey
+import io.legado.app.constant.ReadAloudBgMode
+import io.legado.app.help.config.AppConfigStore
+import io.legado.app.help.config.compatDsInt
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 
 
@@ -13,13 +17,16 @@ class ReadAloudPlayerViewModel(
     private val coordinator: ReadAloudPlayerCoordinator,
     private val coordinator: ReadAloudPlayerCoordinator,
 ) : ViewModel() {
 ) : ViewModel() {
 
 
-    val uiState = coordinator.state
-        .map(::toUiState)
-        .stateIn(
-            scope = viewModelScope,
-            started = SharingStarted.WhileSubscribed(5000),
-            initialValue = toUiState(coordinator.snapshot()),
-        )
+    val uiState = combine(
+        coordinator.state,
+        AppConfigStore.observeInt(PreferKey.readAloudPlayerBgMode),
+    ) { source, bgMode ->
+        toUiState(source, bgMode ?: ReadAloudBgMode.Blur)
+    }.stateIn(
+        scope = viewModelScope,
+        started = SharingStarted.WhileSubscribed(5000),
+        initialValue = toUiState(coordinator.snapshot(), readBgMode()),
+    )
 
 
     private val _effects = MutableSharedFlow<ReadAloudPlayerEffect>(extraBufferCapacity = 8)
     private val _effects = MutableSharedFlow<ReadAloudPlayerEffect>(extraBufferCapacity = 8)
     val effects = _effects.asSharedFlow()
     val effects = _effects.asSharedFlow()
@@ -35,6 +42,7 @@ class ReadAloudPlayerViewModel(
             ReadAloudPlayerIntent.OpenSettings -> effect(ReadAloudPlayerEffect.ReturnToReaderSettings)
             ReadAloudPlayerIntent.OpenSettings -> effect(ReadAloudPlayerEffect.ReturnToReaderSettings)
             ReadAloudPlayerIntent.SwitchToClassic -> effect(ReadAloudPlayerEffect.ReturnToClassic)
             ReadAloudPlayerIntent.SwitchToClassic -> effect(ReadAloudPlayerEffect.ReturnToClassic)
             ReadAloudPlayerIntent.OpenToc -> effect(ReadAloudPlayerEffect.OpenToc)
             ReadAloudPlayerIntent.OpenToc -> effect(ReadAloudPlayerEffect.OpenToc)
+            ReadAloudPlayerIntent.CycleBgMode -> cycleBgMode()
             is ReadAloudPlayerIntent.SetSpeed -> viewModelScope.launch {
             is ReadAloudPlayerIntent.SetSpeed -> viewModelScope.launch {
                 coordinator.setSpeed(intent.value)
                 coordinator.setSpeed(intent.value)
             }
             }
@@ -46,7 +54,20 @@ class ReadAloudPlayerViewModel(
         }
         }
     }
     }
 
 
-    private fun toUiState(source: ReadAloudPlayerSourceState): ReadAloudPlayerUiState {
+    private fun cycleBgMode() {
+        val current = readBgMode()
+        val next = when (current) {
+            ReadAloudBgMode.Solid -> ReadAloudBgMode.Blur
+            ReadAloudBgMode.Blur -> ReadAloudBgMode.FlowingLight
+            else -> ReadAloudBgMode.Solid
+        }
+        AppConfigStore.putInt(PreferKey.readAloudPlayerBgMode, next)
+    }
+
+    private fun toUiState(
+        source: ReadAloudPlayerSourceState,
+        bgMode: Int,
+    ): ReadAloudPlayerUiState {
         val activeIndex = source.textLines.indexOfLast {
         val activeIndex = source.textLines.indexOfLast {
             it.chapterPosition <= source.chapterPosition
             it.chapterPosition <= source.chapterPosition
         }
         }
@@ -70,10 +91,16 @@ class ReadAloudPlayerViewModel(
             isPaused = source.isPaused,
             isPaused = source.isPaused,
             speed = source.speed,
             speed = source.speed,
             timerMinutes = source.timerMinutes,
             timerMinutes = source.timerMinutes,
+            bgMode = bgMode,
         )
         )
     }
     }
 
 
     private fun effect(value: ReadAloudPlayerEffect) {
     private fun effect(value: ReadAloudPlayerEffect) {
         _effects.tryEmit(value)
         _effects.tryEmit(value)
     }
     }
+
+    private fun readBgMode(): Int {
+        return AppConfigStore.preferences.compatDsInt(PreferKey.readAloudPlayerBgMode)
+            ?: ReadAloudBgMode.Blur
+    }
 }
 }

+ 1 - 1
app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfigContract.kt

@@ -19,7 +19,7 @@ data class ReadConfigUiState(
     val useZhLayout: Boolean = false,
     val useZhLayout: Boolean = false,
     val showBrightnessView: String = "0",
     val showBrightnessView: String = "0",
     val brightnessVwPos: String = "1",
     val brightnessVwPos: String = "1",
-    val brightnessAuto: Boolean = false,
+    val brightnessAuto: Boolean = true,
     val useUnderline: Boolean = false,
     val useUnderline: Boolean = false,
     val readSliderMode: String = "0",
     val readSliderMode: String = "0",
     val doubleHorizontalPage: String = "0",
     val doubleHorizontalPage: String = "0",

+ 6 - 5
app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectBackground.kt

@@ -15,7 +15,6 @@ import io.legado.app.ui.util.shouldShowSplitPane
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.isActive
 import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported
 import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported
-import top.yukonga.miuix.kmp.theme.MiuixTheme
 import kotlin.math.floor
 import kotlin.math.floor
 
 
 @Composable
 @Composable
@@ -24,8 +23,10 @@ fun BgEffectBackground(
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
     bgModifier: Modifier = Modifier,
     bgModifier: Modifier = Modifier,
     isFullSize: Boolean = false,
     isFullSize: Boolean = false,
+    drawSurface: Boolean = true,
     effectBackground: Boolean = true,
     effectBackground: Boolean = true,
     isOs3Effect: Boolean = true,
     isOs3Effect: Boolean = true,
+    customPreset: BgEffectConfig.Config? = null,
     alpha: () -> Float = { 1f },
     alpha: () -> Float = { 1f },
     content: @Composable (BoxScope.() -> Unit),
     content: @Composable (BoxScope.() -> Unit),
 ) {
 ) {
@@ -37,14 +38,15 @@ fun BgEffectBackground(
     Box(
     Box(
         modifier = modifier,
         modifier = modifier,
     ) {
     ) {
-        val surface = MiuixTheme.colorScheme.surface
+        val surface = LegadoTheme.colorScheme.surface
         val deviceType = if (shouldShowSplitPane()) DeviceType.PAD else DeviceType.PHONE
         val deviceType = if (shouldShowSplitPane()) DeviceType.PAD else DeviceType.PHONE
         val isDarkTheme = LegadoTheme.isDark
         val isDarkTheme = LegadoTheme.isDark
         val painter = remember(isOs3Effect) { BgEffectPainter(isOs3Effect) }
         val painter = remember(isOs3Effect) { BgEffectPainter(isOs3Effect) }
 
 
-        val preset = remember(deviceType, isDarkTheme, isOs3Effect) {
+        val defaultPreset = remember(deviceType, isDarkTheme, isOs3Effect) {
             BgEffectConfig.get(deviceType, isDarkTheme, isOs3Effect)
             BgEffectConfig.get(deviceType, isDarkTheme, isOs3Effect)
         }
         }
+        val preset = customPreset ?: defaultPreset
 
 
         val colorStage = remember { Animatable(0f) }
         val colorStage = remember { Animatable(0f) }
 
 
@@ -72,9 +74,8 @@ fun BgEffectBackground(
                 .bgEffectDraw(
                 .bgEffectDraw(
                     painter = painter,
                     painter = painter,
                     preset = preset,
                     preset = preset,
-                    deviceType = deviceType,
-                    isDarkTheme = isDarkTheme,
                     surface = surface,
                     surface = surface,
+                    drawSurface = drawSurface,
                     effectBackground = effectBackground,
                     effectBackground = effectBackground,
                     isFullSize = isFullSize,
                     isFullSize = isFullSize,
                     playing = dynamicBackground,
                     playing = dynamicBackground,

+ 2 - 2
app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectConfig.kt

@@ -1,8 +1,8 @@
 package io.legado.app.ui.widget.components.effect
 package io.legado.app.ui.widget.components.effect
 
 
-internal object BgEffectConfig {
+object BgEffectConfig {
 
 
-    internal class Config(
+    class Config(
         val points: FloatArray,
         val points: FloatArray,
         val colors1: FloatArray,
         val colors1: FloatArray,
         val colors2: FloatArray,
         val colors2: FloatArray,

+ 10 - 18
app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectModifier.kt

@@ -14,9 +14,8 @@ import kotlinx.coroutines.launch
 internal fun Modifier.bgEffectDraw(
 internal fun Modifier.bgEffectDraw(
     painter: BgEffectPainter,
     painter: BgEffectPainter,
     preset: BgEffectConfig.Config,
     preset: BgEffectConfig.Config,
-    deviceType: DeviceType,
-    isDarkTheme: Boolean,
     surface: Color,
     surface: Color,
+    drawSurface: Boolean,
     effectBackground: Boolean,
     effectBackground: Boolean,
     isFullSize: Boolean,
     isFullSize: Boolean,
     playing: Boolean,
     playing: Boolean,
@@ -25,9 +24,8 @@ internal fun Modifier.bgEffectDraw(
 ): Modifier = this then BgEffectElement(
 ): Modifier = this then BgEffectElement(
     painter = painter,
     painter = painter,
     preset = preset,
     preset = preset,
-    deviceType = deviceType,
-    isDarkTheme = isDarkTheme,
     surface = surface,
     surface = surface,
+    drawSurface = drawSurface,
     effectBackground = effectBackground,
     effectBackground = effectBackground,
     isFullSize = isFullSize,
     isFullSize = isFullSize,
     playing = playing,
     playing = playing,
@@ -38,9 +36,8 @@ internal fun Modifier.bgEffectDraw(
 private data class BgEffectElement(
 private data class BgEffectElement(
     val painter: BgEffectPainter,
     val painter: BgEffectPainter,
     val preset: BgEffectConfig.Config,
     val preset: BgEffectConfig.Config,
-    val deviceType: DeviceType,
-    val isDarkTheme: Boolean,
     val surface: Color,
     val surface: Color,
+    val drawSurface: Boolean,
     val effectBackground: Boolean,
     val effectBackground: Boolean,
     val isFullSize: Boolean,
     val isFullSize: Boolean,
     val playing: Boolean,
     val playing: Boolean,
@@ -51,9 +48,8 @@ private data class BgEffectElement(
     override fun create(): BgEffectNode = BgEffectNode(
     override fun create(): BgEffectNode = BgEffectNode(
         painter = painter,
         painter = painter,
         preset = preset,
         preset = preset,
-        deviceType = deviceType,
-        isDarkTheme = isDarkTheme,
         surface = surface,
         surface = surface,
+        drawSurface = drawSurface,
         effectBackground = effectBackground,
         effectBackground = effectBackground,
         isFullSize = isFullSize,
         isFullSize = isFullSize,
         playing = playing,
         playing = playing,
@@ -65,9 +61,8 @@ private data class BgEffectElement(
         node.update(
         node.update(
             painter = painter,
             painter = painter,
             preset = preset,
             preset = preset,
-            deviceType = deviceType,
-            isDarkTheme = isDarkTheme,
             surface = surface,
             surface = surface,
+            drawSurface = drawSurface,
             effectBackground = effectBackground,
             effectBackground = effectBackground,
             isFullSize = isFullSize,
             isFullSize = isFullSize,
             playing = playing,
             playing = playing,
@@ -80,9 +75,8 @@ private data class BgEffectElement(
 private class BgEffectNode(
 private class BgEffectNode(
     private var painter: BgEffectPainter,
     private var painter: BgEffectPainter,
     private var preset: BgEffectConfig.Config,
     private var preset: BgEffectConfig.Config,
-    private var deviceType: DeviceType,
-    private var isDarkTheme: Boolean,
     private var surface: Color,
     private var surface: Color,
+    private var drawSurface: Boolean,
     private var effectBackground: Boolean,
     private var effectBackground: Boolean,
     private var isFullSize: Boolean,
     private var isFullSize: Boolean,
     private var playing: Boolean,
     private var playing: Boolean,
@@ -107,9 +101,8 @@ private class BgEffectNode(
     fun update(
     fun update(
         painter: BgEffectPainter,
         painter: BgEffectPainter,
         preset: BgEffectConfig.Config,
         preset: BgEffectConfig.Config,
-        deviceType: DeviceType,
-        isDarkTheme: Boolean,
         surface: Color,
         surface: Color,
+        drawSurface: Boolean,
         effectBackground: Boolean,
         effectBackground: Boolean,
         isFullSize: Boolean,
         isFullSize: Boolean,
         playing: Boolean,
         playing: Boolean,
@@ -118,9 +111,8 @@ private class BgEffectNode(
     ) {
     ) {
         this.painter = painter
         this.painter = painter
         this.preset = preset
         this.preset = preset
-        this.deviceType = deviceType
-        this.isDarkTheme = isDarkTheme
         this.surface = surface
         this.surface = surface
+        this.drawSurface = drawSurface
         this.effectBackground = effectBackground
         this.effectBackground = effectBackground
         this.isFullSize = isFullSize
         this.isFullSize = isFullSize
         this.colorStage = colorStage
         this.colorStage = colorStage
@@ -156,7 +148,7 @@ private class BgEffectNode(
     }
     }
 
 
     override fun ContentDrawScope.draw() {
     override fun ContentDrawScope.draw() {
-        drawRect(surface)
+        if (drawSurface) drawRect(surface)
         if (effectBackground) {
         if (effectBackground) {
             val alphaValue = alpha()
             val alphaValue = alpha()
             if (alphaValue > 0f) {
             if (alphaValue > 0f) {
@@ -164,7 +156,7 @@ private class BgEffectNode(
 
 
                 painter.updateResolution(size.width, size.height)
                 painter.updateResolution(size.width, size.height)
                 painter.updateBoundIfNeeded(drawHeight, size.height, size.width)
                 painter.updateBoundIfNeeded(drawHeight, size.height, size.width)
-                painter.updatePresetIfNeeded(deviceType, isDarkTheme)
+                painter.updatePresetIfNeeded(preset)
                 painter.updateColors(preset, colorStage())
                 painter.updateColors(preset, colorStage())
                 painter.updateAnimTime(animTime)
                 painter.updateAnimTime(animTime)
                 painter.updatePointsAnim(animTime, preset)
                 painter.updatePointsAnim(animTime, preset)

+ 4 - 16
app/src/main/java/io/legado/app/ui/widget/components/effect/BgEffectPainter.kt

@@ -28,10 +28,7 @@ internal class BgEffectPainter(
     private val pointsAnimBuffer = FloatArray(8)
     private val pointsAnimBuffer = FloatArray(8)
 
 
     private var animTime = Float.NaN
     private var animTime = Float.NaN
-    private var isDarkCached: Boolean? = null
-    private var deviceTypeCached: DeviceType? = null
-
-    private var presetApplied = false
+    private var staticUniformPreset: BgEffectConfig.Config? = null
 
 
     private var cachedLogoHeight = Float.NaN
     private var cachedLogoHeight = Float.NaN
     private var cachedTotalHeight = Float.NaN
     private var cachedTotalHeight = Float.NaN
@@ -134,22 +131,13 @@ internal class BgEffectPainter(
         cachedTotalWidth = totalWidth
         cachedTotalWidth = totalWidth
     }
     }
 
 
-    fun updatePresetIfNeeded(deviceType: DeviceType, isDark: Boolean) {
-        if (presetApplied && isDarkCached == isDark && deviceTypeCached == deviceType) return
-
-        applyPreset(deviceType, isDark)
-
-        isDarkCached = isDark
-        deviceTypeCached = deviceType
-        presetApplied = true
-    }
-
-    private fun applyPreset(deviceType: DeviceType, isDark: Boolean) {
-        val preset = BgEffectConfig.get(deviceType, isDark, isOs3)
+    fun updatePresetIfNeeded(preset: BgEffectConfig.Config) {
+        if (staticUniformPreset === preset) return
 
 
         runtimeShader.setFloatUniform("uPoints", preset.points)
         runtimeShader.setFloatUniform("uPoints", preset.points)
         runtimeShader.setFloatUniform("uLightOffset", preset.lightOffset)
         runtimeShader.setFloatUniform("uLightOffset", preset.lightOffset)
         runtimeShader.setFloatUniform("uSaturateOffset", preset.saturateOffset)
         runtimeShader.setFloatUniform("uSaturateOffset", preset.saturateOffset)
+        staticUniformPreset = preset
     }
     }
 
 
     private fun updateBound(
     private fun updateBound(

+ 12 - 4
app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoverBlurBackdrop.kt

@@ -6,8 +6,9 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.blur
 import androidx.compose.ui.draw.blur
-import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.graphics.lerp
 import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import coil.size.Size
 import coil.size.Size
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
@@ -19,6 +20,7 @@ fun CoverBlurBackdrop(
     path: String?,
     path: String?,
     sourceOrigin: String?,
     sourceOrigin: String?,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
+    blurRadius: Dp = 48.dp,
 ) {
 ) {
     val tint = lerp(LegadoTheme.colorScheme.secondaryContainer, LegadoTheme.seedColor, 0.42f)
     val tint = lerp(LegadoTheme.colorScheme.secondaryContainer, LegadoTheme.seedColor, 0.42f)
     Box(modifier.fillMaxSize()) {
     Box(modifier.fillMaxSize()) {
@@ -28,12 +30,18 @@ fun CoverBlurBackdrop(
             path = path,
             path = path,
             sourceOrigin = sourceOrigin,
             sourceOrigin = sourceOrigin,
             memoryCacheKey = path?.let { "$it#cover-blur-backdrop" },
             memoryCacheKey = path?.let { "$it#cover-blur-backdrop" },
-            modifier = Modifier.fillMaxSize().blur(32.dp),
+            modifier = Modifier
+                .fillMaxSize()
+                .blur(blurRadius),
             contentScale = ContentScale.Crop,
             contentScale = ContentScale.Crop,
             showLoadingPlaceholder = false,
             showLoadingPlaceholder = false,
             requestBuilder = { size(Size(512, 512)) },
             requestBuilder = { size(Size(512, 512)) },
         )
         )
-        Box(Modifier.fillMaxSize().background(tint.copy(alpha = 0.38f)))
-        Box(Modifier.fillMaxSize().background(LegadoTheme.colorScheme.surface.copy(alpha = 0.52f)))
+        Box(Modifier
+            .fillMaxSize()
+            .background(tint.copy(alpha = 0.38f)))
+        Box(Modifier
+            .fillMaxSize()
+            .background(LegadoTheme.colorScheme.surface.copy(alpha = 0.52f)))
     }
     }
 }
 }

+ 3 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -2826,4 +2826,7 @@
     <string name="tts_cache_unknown_text">未知文本</string>
     <string name="tts_cache_unknown_text">未知文本</string>
     <string name="capsule_auto_collapse">自动收缩胶囊</string>
     <string name="capsule_auto_collapse">自动收缩胶囊</string>
     <string name="capsule_auto_collapse_summary">3秒无操作后收缩为迷你胶囊</string>
     <string name="capsule_auto_collapse_summary">3秒无操作后收缩为迷你胶囊</string>
+    <string name="read_aloud_bg_solid">纯色</string>
+    <string name="read_aloud_bg_blur">模糊</string>
+    <string name="read_aloud_bg_flowing_light">流光</string>
 </resources>
 </resources>

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -2831,4 +2831,7 @@
     <string name="tts_cache_unknown_text">Unknown text</string>
     <string name="tts_cache_unknown_text">Unknown text</string>
     <string name="capsule_auto_collapse">Auto-collapse capsule</string>
     <string name="capsule_auto_collapse">Auto-collapse capsule</string>
     <string name="capsule_auto_collapse_summary">Collapse to mini capsule after 3 seconds of inactivity</string>
     <string name="capsule_auto_collapse_summary">Collapse to mini capsule after 3 seconds of inactivity</string>
+    <string name="read_aloud_bg_solid">Solid</string>
+    <string name="read_aloud_bg_blur">Blur</string>
+    <string name="read_aloud_bg_flowing_light">Flowing light</string>
 </resources>
 </resources>