Przeglądaj źródła

refactor: 阅读菜单视觉与状态逻辑

HapeLee 3 miesięcy temu
rodzic
commit
1ec2a4c6e3

+ 1 - 0
app/src/main/java/io/legado/app/ui/animation/InteractiveHighlight.kt

@@ -37,6 +37,7 @@ class InteractiveHighlight(
     private var startPosition = Offset.Zero
     private var startPosition = Offset.Zero
     val pressProgress: Float get() = pressProgressAnimation.value
     val pressProgress: Float get() = pressProgressAnimation.value
     val offset: Offset get() = positionAnimation.value
     val offset: Offset get() = positionAnimation.value
+    val dragOffset: Offset get() = positionAnimation.value - startPosition
 
 
     @Language("AGSL")
     @Language("AGSL")
     private val shader = RuntimeShader(
     private val shader = RuntimeShader(

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

@@ -81,6 +81,8 @@ data class ReadBookStyleConfig(
     val textColor: String = "#3E3D3B",
     val textColor: String = "#3E3D3B",
     val textColorNight: String = "#CCCCCC",
     val textColorNight: String = "#CCCCCC",
     val textColorEInk: String = "#000000",
     val textColorEInk: String = "#000000",
+    val textFont: String = "",
+    val titleFont: String = "",
     // Page anim
     // Page anim
     val pageAnim: Int = 0,
     val pageAnim: Int = 0,
     val pageAnimEInk: Int = 4,
     val pageAnimEInk: Int = 4,

+ 9 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt

@@ -103,6 +103,15 @@ class ReadBookController(
         pageChanged = false
         pageChanged = false
     }
     }
 
 
+    override fun previewBrightness(value: Int) {
+        val targetBrightness = value.coerceIn(0, 100) / 100f
+        val attributes = activity.window.attributes
+        if (attributes.screenBrightness != targetBrightness) {
+            attributes.screenBrightness = targetBrightness
+            activity.window.attributes = attributes
+        }
+    }
+
     // Callbacks to Activity for operations that require Activity-level state
     // Callbacks to Activity for operations that require Activity-level state
     var onScreenOffTimerStart: (() -> Unit)? = null
     var onScreenOffTimerStart: (() -> Unit)? = null
     var onStartContentLoadFinish: (() -> Unit)? = null
     var onStartContentLoadFinish: (() -> Unit)? = null

+ 169 - 234
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt

@@ -1,8 +1,6 @@
 package io.legado.app.ui.book.read
 package io.legado.app.ui.book.read
 
 
 import android.content.Context
 import android.content.Context
-import android.os.Build
-import androidx.activity.compose.LocalActivity
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.SizeTransform
 import androidx.compose.animation.SizeTransform
@@ -102,7 +100,6 @@ import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.draw.drawWithCache
 import androidx.compose.ui.draw.drawWithCache
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Outline
 import androidx.compose.ui.graphics.Outline
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.Path
@@ -147,15 +144,11 @@ import com.kyant.backdrop.backdrops.rememberLayerBackdrop
 import com.kyant.backdrop.drawBackdrop
 import com.kyant.backdrop.drawBackdrop
 import com.kyant.backdrop.effects.blur
 import com.kyant.backdrop.effects.blur
 import com.kyant.backdrop.effects.lens
 import com.kyant.backdrop.effects.lens
-import com.kyant.backdrop.effects.vibrancy
 import com.kyant.backdrop.highlight.Highlight
 import com.kyant.backdrop.highlight.Highlight
 import com.kyant.backdrop.shadow.InnerShadow
 import com.kyant.backdrop.shadow.InnerShadow
 import com.kyant.backdrop.shadow.Shadow
 import com.kyant.backdrop.shadow.Shadow
 import com.kyant.capsule.ContinuousCapsule
 import com.kyant.capsule.ContinuousCapsule
-import dev.chrisbanes.haze.HazeProgressive
 import dev.chrisbanes.haze.HazeState
 import dev.chrisbanes.haze.HazeState
-import dev.chrisbanes.haze.hazeEffect
-import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.constant.ReadMenuBlurMode
 import io.legado.app.constant.ReadMenuBlurMode
 import io.legado.app.constant.ReadMenuBlurStyle
 import io.legado.app.constant.ReadMenuBlurStyle
@@ -163,7 +156,6 @@ import io.legado.app.data.entities.Book
 import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.help.config.ReadStyleResolver
 import io.legado.app.help.config.ReadStyleResolver
 import io.legado.app.ui.animation.DampedDragAnimation
 import io.legado.app.ui.animation.DampedDragAnimation
-import io.legado.app.ui.animation.InteractiveHighlight
 import io.legado.app.ui.book.read.sheet.AutoReadContent
 import io.legado.app.ui.book.read.sheet.AutoReadContent
 import io.legado.app.ui.book.read.sheet.HeaderFooterPage
 import io.legado.app.ui.book.read.sheet.HeaderFooterPage
 import io.legado.app.ui.book.read.sheet.PaddingConfigContent
 import io.legado.app.ui.book.read.sheet.PaddingConfigContent
@@ -174,7 +166,6 @@ import io.legado.app.ui.book.read.sheet.ReadStyleTextTitleContent
 import io.legado.app.ui.book.read.sheet.readMenuButtonInfos
 import io.legado.app.ui.book.read.sheet.readMenuButtonInfos
 import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.ui.config.readConfig.ReadConfig
 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.widget.components.AppSlider
 import io.legado.app.ui.widget.components.AppSlider
 import io.legado.app.ui.widget.components.AppVerticalSlider
 import io.legado.app.ui.widget.components.AppVerticalSlider
 import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.button.series.SmallTonalButton
@@ -182,14 +173,17 @@ import io.legado.app.ui.widget.components.divider.PillDivider
 import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
 import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
+import io.legado.app.ui.widget.components.reader.ReaderMenuEffect
+import io.legado.app.ui.widget.components.reader.ReaderMenuPlacement
+import io.legado.app.ui.widget.components.reader.ReaderMenuTintStyle
+import io.legado.app.ui.widget.components.reader.ReaderMenuVisualState
+import io.legado.app.ui.widget.components.reader.readerMenuHazeEffect
+import io.legado.app.ui.widget.components.reader.readerMenuLiquidGlass
+import io.legado.app.ui.widget.components.reader.readerMenuLiquidGlassAvailable
+import io.legado.app.ui.widget.components.reader.readerMenuSurfaceBrush
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
-import kotlin.math.abs
-import kotlin.math.atan2
 import kotlin.math.ceil
 import kotlin.math.ceil
-import kotlin.math.cos
 import kotlin.math.roundToInt
 import kotlin.math.roundToInt
-import kotlin.math.sin
-import kotlin.math.tanh
 
 
 /**
 /**
  * Compose replacement for ReadMenu — main reading menu overlay.
  * Compose replacement for ReadMenu — main reading menu overlay.
@@ -199,6 +193,7 @@ fun ReadBookMenuBar(
     state: ReadBookUiState,
     state: ReadBookUiState,
     preferences: ReadPreferences,
     preferences: ReadPreferences,
     onIntent: (ReadBookIntent) -> Unit,
     onIntent: (ReadBookIntent) -> Unit,
+    onBrightnessPreview: (Int) -> Unit,
     backdrop: Backdrop? = null,
     backdrop: Backdrop? = null,
     hazeState: HazeState? = null,
     hazeState: HazeState? = null,
 ) {
 ) {
@@ -221,7 +216,7 @@ fun ReadBookMenuBar(
     }
     }
     val hideTopBar = dialogLikeRoute ||
     val hideTopBar = dialogLikeRoute ||
             currentRoute == ReadBookMenuRoute.TextTitle
             currentRoute == ReadBookMenuRoute.TextTitle
-    val menuColors = readMenuColors()
+    val menuColors = readMenuColors(preferences.readBarStyle)
 
 
     Box(Modifier.fillMaxSize()) {
     Box(Modifier.fillMaxSize()) {
         AnimatedVisibility(
         AnimatedVisibility(
@@ -259,6 +254,7 @@ fun ReadBookMenuBar(
                     onIntent = onIntent,
                     onIntent = onIntent,
                     backdrop = backdrop,
                     backdrop = backdrop,
                     hazeState = hazeState,
                     hazeState = hazeState,
+                    titleBarMode = preferences.titleBarMode,
                 )
                 )
                 if (state.menuConfig.showTitleBarIcons && state.menuConfig.titleBarIconPosition <= 1) {
                 if (state.menuConfig.showTitleBarIcons && state.menuConfig.titleBarIconPosition <= 1) {
                     FloatingIconRow(
                     FloatingIconRow(
@@ -286,6 +282,13 @@ fun ReadBookMenuBar(
             menuConfig = state.menuConfig,
             menuConfig = state.menuConfig,
             isFloating = false,
             isFloating = false,
         )
         )
+        val brightnessVisualState = ReaderMenuVisualState(
+            effect = if (useBrightnessHaze) ReaderMenuEffect.Haze else ReaderMenuEffect.None,
+            tintStyle = ReaderMenuTintStyle.Fill,
+            styleEnabled = false,
+            tintAllowed = true,
+            tintFill = false,
+        )
         AnimatedVisibility(
         AnimatedVisibility(
             visible = brightnessMode == "2" && state.menuVisible && currentRoute == ReadBookMenuRoute.Main,
             visible = brightnessMode == "2" && state.menuVisible && currentRoute == ReadBookMenuRoute.Main,
             enter = slideInHorizontally(
             enter = slideInHorizontally(
@@ -314,7 +317,7 @@ fun ReadBookMenuBar(
                             colors = menuColors,
                             colors = menuColors,
                             shape = brightnessShape,
                             shape = brightnessShape,
                             menuConfig = state.menuConfig,
                             menuConfig = state.menuConfig,
-                            progressive = false,
+                            visualState = brightnessVisualState,
                         )
                         )
                     } else {
                     } else {
                         Modifier
                         Modifier
@@ -350,6 +353,7 @@ fun ReadBookMenuBar(
                             menuConfig = state.menuConfig,
                             menuConfig = state.menuConfig,
                         ),
                         ),
                         glassThumbEnabled = false,
                         glassThumbEnabled = false,
+                        onBrightnessPreview = onBrightnessPreview,
                     )
                     )
                 }
                 }
             }
             }
@@ -390,6 +394,8 @@ fun ReadBookMenuBar(
                     hazeState = hazeState,
                     hazeState = hazeState,
                     readStylePage = readStylePage,
                     readStylePage = readStylePage,
                     onReadStylePageChanged = { readStylePage = it },
                     onReadStylePageChanged = { readStylePage = it },
+                    progressBarBehavior = preferences.progressBarBehavior,
+                    onBrightnessPreview = onBrightnessPreview,
                 )
                 )
             }
             }
         }
         }
@@ -413,6 +419,8 @@ private fun ReadBookMenuSurface(
     hazeState: HazeState?,
     hazeState: HazeState?,
     readStylePage: Int,
     readStylePage: Int,
     onReadStylePageChanged: (Int) -> Unit,
     onReadStylePageChanged: (Int) -> Unit,
+    progressBarBehavior: String,
+    onBrightnessPreview: (Int) -> Unit,
 ) {
 ) {
     val route = when (contentTarget) {
     val route = when (contentTarget) {
         ReadBookMenuContent.Search -> ReadBookMenuRoute.Main
         ReadBookMenuContent.Search -> ReadBookMenuRoute.Main
@@ -505,10 +513,26 @@ private fun ReadBookMenuSurface(
         menuConfig = state.menuConfig,
         menuConfig = state.menuConfig,
     )
     )
     val useLens = useLiquidGlass && isFloating && mainCorner > 0.dp
     val useLens = useLiquidGlass && isFloating && mainCorner > 0.dp
-    val bottomBarProgressiveBlur = route == ReadBookMenuRoute.Main &&
-            !isFloating &&
-            state.menuConfig.readMenuBottomBarBlurStyle == ReadMenuBlurStyle.Progressive
-    val bottomBarTextColor = if (bottomBarProgressiveBlur) {
+    val bottomBarVisualState = ReaderMenuVisualState(
+        effect = when {
+            useLiquidGlass -> ReaderMenuEffect.LiquidGlass
+            useHaze -> ReaderMenuEffect.Haze
+            else -> ReaderMenuEffect.None
+        },
+        tintStyle = state.menuConfig.readMenuBottomBarBlurStyle.toReaderMenuTintStyle(),
+        styleEnabled = route == ReadBookMenuRoute.Main && !isFloating,
+        tintAllowed = route == ReadBookMenuRoute.Main,
+        tintFill = false,
+    )
+    val bottomBarMenuTintColor = readMenuTintColor(state.menuConfig)
+        .takeIf { bottomBarVisualState.useTint }
+    val bottomBarSurfaceAlpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
+    val bottomBarFillAlpha = if (expanded) {
+        bottomBarSurfaceAlpha.coerceAtLeast(0.85f)
+    } else {
+        bottomBarSurfaceAlpha
+    }
+    val bottomBarTextColor = if (bottomBarVisualState.useContrastContent) {
         Color.White.copy(alpha = 0.87f).compositeOver(colors.background)
         Color.White.copy(alpha = 0.87f).compositeOver(colors.background)
     } else {
     } else {
         LegadoTheme.colorScheme.onSurface
         LegadoTheme.colorScheme.onSurface
@@ -562,13 +586,38 @@ private fun ReadBookMenuSurface(
                         colors = colors,
                         colors = colors,
                         shape = surfaceShape,
                         shape = surfaceShape,
                         menuConfig = state.menuConfig,
                         menuConfig = state.menuConfig,
-                        progressive = bottomBarProgressiveBlur,
+                        visualState = bottomBarVisualState,
                         blurRadiusDp = if (expanded) 32 else null,
                         blurRadiusDp = if (expanded) 32 else null,
                     )
                     )
                 } else {
                 } else {
                     Modifier
                     Modifier
                 }
                 }
             )
             )
+            .then(
+                if (!useLiquidGlass && !useHaze) {
+                    Modifier
+                        .clip(surfaceShape)
+                        .background(
+                            if (bottomBarVisualState.isGradient) {
+                                readerMenuSurfaceBrush(
+                                    style = ReaderMenuTintStyle.Gradient,
+                                    placement = ReaderMenuPlacement.Bottom,
+                                    color = bottomBarMenuTintColor ?: colors.background,
+                                    alpha = bottomBarFillAlpha,
+                                )
+                            } else {
+                                readerMenuSurfaceBrush(
+                                    style = ReaderMenuTintStyle.Fill,
+                                    placement = ReaderMenuPlacement.Bottom,
+                                    color = colors.background,
+                                    alpha = bottomBarFillAlpha,
+                                )
+                            }
+                        )
+                } else {
+                    Modifier
+                }
+            )
             .drawWithCache {
             .drawWithCache {
                 val strokeWidthPx = bottomBarBorderWidth.dp.toPx()
                 val strokeWidthPx = bottomBarBorderWidth.dp.toPx()
                 val outline = surfaceShape.createOutline(size, layoutDirection, this)
                 val outline = surfaceShape.createOutline(size, layoutDirection, this)
@@ -589,11 +638,7 @@ private fun ReadBookMenuSurface(
                 }
                 }
             },
             },
         shape = surfaceShape,
         shape = surfaceShape,
-        color = if (useLiquidGlass || useHaze) Color.Transparent else {
-            val baseAlpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
-            val effectiveAlpha = if (expanded) baseAlpha.coerceAtLeast(0.85f) else baseAlpha
-            colors.background.copy(alpha = effectiveAlpha)
-        },
+        color = Color.Transparent,
         contentColor = colors.content
         contentColor = colors.content
     ) {
     ) {
         AnimatedContent(
         AnimatedContent(
@@ -628,6 +673,8 @@ private fun ReadBookMenuSurface(
                         buttonGlassEnabled = useBottomBarButtonGlass,
                         buttonGlassEnabled = useBottomBarButtonGlass,
                         backdrop = backdrop,
                         backdrop = backdrop,
                         labelColor = bottomBarTextColor,
                         labelColor = bottomBarTextColor,
+                        progressBarBehavior = progressBarBehavior,
+                        onBrightnessPreview = onBrightnessPreview,
                     )
                     )
                 }
                 }
 
 
@@ -852,9 +899,8 @@ private fun MenuTitleBar(
     onIntent: (ReadBookIntent) -> Unit,
     onIntent: (ReadBookIntent) -> Unit,
     backdrop: Backdrop?,
     backdrop: Backdrop?,
     hazeState: HazeState?,
     hazeState: HazeState?,
+    titleBarMode: String,
 ) {
 ) {
-    val titleBarMode = ReadConfig.titleBarMode
-
     var expanded by remember { mutableStateOf(false) }
     var expanded by remember { mutableStateOf(false) }
 
 
     val topBarBorderWidth = state.menuConfig.readMenuBorderWidth
     val topBarBorderWidth = state.menuConfig.readMenuBorderWidth
@@ -866,15 +912,22 @@ private fun MenuTitleBar(
         ?: LegadoTheme.colorScheme.outlineVariant.hashCode()
         ?: LegadoTheme.colorScheme.outlineVariant.hashCode()
     val topBarAlpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
     val topBarAlpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
     val useTopBarBlur = readMenuTopBarHazeEnabled(hazeState, state.menuConfig)
     val useTopBarBlur = readMenuTopBarHazeEnabled(hazeState, state.menuConfig)
-    val topBarProgressiveBlur = state.menuConfig.readMenuTopBarBlurStyle ==
-            ReadMenuBlurStyle.Progressive
-    val progressiveBlurActive = useTopBarBlur && topBarProgressiveBlur
-    val titleTextColor = if (progressiveBlurActive) {
+    val topBarVisualState = ReaderMenuVisualState(
+        effect = if (useTopBarBlur) ReaderMenuEffect.Haze else ReaderMenuEffect.None,
+        tintStyle = state.menuConfig.readMenuTopBarBlurStyle.toReaderMenuTintStyle(),
+        styleEnabled = true,
+        tintAllowed = true,
+        tintFill = true,
+    )
+    val topBarTintColor = readMenuTintColor(state.menuConfig)
+        .takeIf { topBarVisualState.useTint }
+        ?: colors.background
+    val titleTextColor = if (topBarVisualState.useContrastContent) {
         Color.White.copy(alpha = 0.72f).compositeOver(colors.background)
         Color.White.copy(alpha = 0.72f).compositeOver(colors.background)
     } else {
     } else {
         LegadoTheme.colorScheme.onSurface
         LegadoTheme.colorScheme.onSurface
     }
     }
-    val labelStyle = if (progressiveBlurActive) {
+    val labelStyle = if (topBarVisualState.useContrastContent) {
         LegadoTheme.typography.labelSmallEmphasized.copy(
         LegadoTheme.typography.labelSmallEmphasized.copy(
             shadow = androidx.compose.ui.graphics.Shadow(
             shadow = androidx.compose.ui.graphics.Shadow(
                 color = Color.Black.copy(alpha = 0.12f),
                 color = Color.Black.copy(alpha = 0.12f),
@@ -891,25 +944,33 @@ private fun MenuTitleBar(
             .fillMaxWidth()
             .fillMaxWidth()
             .then(
             .then(
                 if (useTopBarBlur && hazeState != null) {
                 if (useTopBarBlur && hazeState != null) {
-                    Modifier
-                        .background(
-                            if (topBarProgressiveBlur) {
-                                readMenuTopBarSurfaceBrush(
-                                    colors = colors,
-                                    alpha = topBarAlpha,
-                                )
-                            } else {
-                                readMenuTopBarSurfaceBrush(colors, topBarAlpha)
-                            }
-                        )
-                        .readMenuHazeEffect(
-                            state = hazeState,
-                            colors = colors,
-                            menuConfig = state.menuConfig,
-                            progressive = topBarProgressiveBlur,
-                        )
+                    Modifier.readerMenuHazeEffect(
+                        state = hazeState,
+                        visualState = topBarVisualState,
+                        placement = ReaderMenuPlacement.Top,
+                        baseColor = colors.background,
+                        tintColor = readMenuTintColor(state.menuConfig),
+                        blurRadius = state.menuConfig.readMenuBlurRadius,
+                        surfaceAlpha = state.menuConfig.readMenuBlurAlpha,
+                    )
                 } else {
                 } else {
-                    Modifier.background(colors.background.copy(alpha = topBarAlpha))
+                    Modifier.background(
+                        if (topBarVisualState.isGradient) {
+                            readerMenuSurfaceBrush(
+                                style = ReaderMenuTintStyle.Gradient,
+                                placement = ReaderMenuPlacement.Top,
+                                color = topBarTintColor,
+                                alpha = topBarAlpha,
+                            )
+                        } else {
+                            readerMenuSurfaceBrush(
+                                style = ReaderMenuTintStyle.Fill,
+                                placement = ReaderMenuPlacement.Top,
+                                color = topBarTintColor,
+                                alpha = topBarAlpha,
+                            )
+                        }
+                    )
                 }
                 }
             )
             )
             .then(
             .then(
@@ -932,7 +993,7 @@ private fun MenuTitleBar(
             )
             )
     ) {
     ) {
         val useTitleCapsule = readMenuTopBarTitleCapsuleEnabled(backdrop, state.menuConfig)
         val useTitleCapsule = readMenuTopBarTitleCapsuleEnabled(backdrop, state.menuConfig)
-                && progressiveBlurActive
+                && topBarVisualState.isProgressiveBlur
         val capsuleIconColor = LegadoTheme.colorScheme.onSurfaceVariant
         val capsuleIconColor = LegadoTheme.colorScheme.onSurfaceVariant
 
 
         // Title row: left group (back + capsule/title) + right group (actions)
         // Title row: left group (back + capsule/title) + right group (actions)
@@ -2212,6 +2273,8 @@ private fun MenuBottomBar(
     buttonGlassEnabled: Boolean = false,
     buttonGlassEnabled: Boolean = false,
     backdrop: Backdrop? = null,
     backdrop: Backdrop? = null,
     labelColor: Color = LegadoTheme.colorScheme.onSurface,
     labelColor: Color = LegadoTheme.colorScheme.onSurface,
+    progressBarBehavior: String,
+    onBrightnessPreview: (Int) -> Unit,
 ) {
 ) {
     val seekMax = state.seekMax.coerceAtLeast(0)
     val seekMax = state.seekMax.coerceAtLeast(0)
     val sliderMax = seekMax.toFloat().coerceAtLeast(1f)
     val sliderMax = seekMax.toFloat().coerceAtLeast(1f)
@@ -2224,7 +2287,6 @@ private fun MenuBottomBar(
     } else {
     } else {
         0.dp
         0.dp
     }
     }
-    val progressBarBehavior = ReadConfig.progressBarBehavior
     val progressCurrent = sliderValue.roundToInt().coerceIn(0, seekMax) + 1
     val progressCurrent = sliderValue.roundToInt().coerceIn(0, seekMax) + 1
     val progressTotal = seekMax + 1
     val progressTotal = seekMax + 1
     val progressValueDescription = stringResource(
     val progressValueDescription = stringResource(
@@ -2296,6 +2358,7 @@ private fun MenuBottomBar(
                 backdrop = backdrop,
                 backdrop = backdrop,
                 buttonGlassEnabled = buttonGlassEnabled,
                 buttonGlassEnabled = buttonGlassEnabled,
                 glassThumbEnabled = buttonGlassEnabled,
                 glassThumbEnabled = buttonGlassEnabled,
+                onBrightnessPreview = onBrightnessPreview,
             )
             )
             Spacer(Modifier.height(4.dp))
             Spacer(Modifier.height(4.dp))
         }
         }
@@ -2933,8 +2996,12 @@ private data class ReadMenuColors(
     val content: Color,
     val content: Color,
 )
 )
 
 
-private fun readMenuLiquidGlassAvailable(backdrop: Backdrop?): Boolean {
-    return backdrop != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+private fun Int.toReaderMenuTintStyle(): ReaderMenuTintStyle {
+    return if (this == ReadMenuBlurStyle.Progressive) {
+        ReaderMenuTintStyle.Gradient
+    } else {
+        ReaderMenuTintStyle.Fill
+    }
 }
 }
 
 
 private fun readMenuTopBarButtonLiquidGlassEnabled(
 private fun readMenuTopBarButtonLiquidGlassEnabled(
@@ -2943,7 +3010,7 @@ private fun readMenuTopBarButtonLiquidGlassEnabled(
 ): Boolean {
 ): Boolean {
     return menuConfig.readMenuTopBarBlurMode != ReadMenuBlurMode.None &&
     return menuConfig.readMenuTopBarBlurMode != ReadMenuBlurMode.None &&
             menuConfig.readMenuTopBarLiquidGlassButtons &&
             menuConfig.readMenuTopBarLiquidGlassButtons &&
-            readMenuLiquidGlassAvailable(backdrop)
+            readerMenuLiquidGlassAvailable(backdrop)
 }
 }
 
 
 private fun readMenuTopBarTitleCapsuleEnabled(
 private fun readMenuTopBarTitleCapsuleEnabled(
@@ -2952,7 +3019,7 @@ private fun readMenuTopBarTitleCapsuleEnabled(
 ): Boolean {
 ): Boolean {
     return menuConfig.readMenuTopBarBlurMode != ReadMenuBlurMode.None &&
     return menuConfig.readMenuTopBarBlurMode != ReadMenuBlurMode.None &&
             menuConfig.readMenuTopBarTitleCapsule &&
             menuConfig.readMenuTopBarTitleCapsule &&
-            readMenuLiquidGlassAvailable(backdrop)
+            readerMenuLiquidGlassAvailable(backdrop)
 }
 }
 
 
 private fun readMenuBottomBarButtonLiquidGlassEnabled(
 private fun readMenuBottomBarButtonLiquidGlassEnabled(
@@ -2960,7 +3027,7 @@ private fun readMenuBottomBarButtonLiquidGlassEnabled(
     menuConfig: ReadMenuConfig,
     menuConfig: ReadMenuConfig,
 ): Boolean {
 ): Boolean {
     return menuConfig.readMenuBottomBarLiquidGlassButtons &&
     return menuConfig.readMenuBottomBarLiquidGlassButtons &&
-            readMenuLiquidGlassAvailable(backdrop)
+            readerMenuLiquidGlassAvailable(backdrop)
 }
 }
 
 
 private fun readMenuTopBarHazeEnabled(
 private fun readMenuTopBarHazeEnabled(
@@ -2992,7 +3059,7 @@ private fun readMenuBottomBarLiquidGlassEnabled(
                 menuConfig,
                 menuConfig,
                 isFloating
                 isFloating
             ) == ReadMenuBlurMode.LiquidGlass &&
             ) == ReadMenuBlurMode.LiquidGlass &&
-            readMenuLiquidGlassAvailable(backdrop)
+            readerMenuLiquidGlassAvailable(backdrop)
 }
 }
 
 
 private fun readMenuBottomBarHazeEnabled(
 private fun readMenuBottomBarHazeEnabled(
@@ -3015,204 +3082,79 @@ private fun Modifier.readMenuLiquidGlass(
     interactive: Boolean = false,
     interactive: Boolean = false,
     menuConfig: ReadMenuConfig,
     menuConfig: ReadMenuConfig,
 ): Modifier {
 ): Modifier {
-    if (!readMenuLiquidGlassAvailable(backdrop)) return this
-    val animationScope = rememberCoroutineScope()
-    val interactiveHighlight = if (interactive) {
-        remember(animationScope) { InteractiveHighlight(animationScope = animationScope) }
-    } else {
-        null
-    }
     val resolvedBlurRadius = blurRadius ?: menuConfig.readMenuBlurRadius.dp
     val resolvedBlurRadius = blurRadius ?: menuConfig.readMenuBlurRadius.dp
     val blurAlpha = menuConfig.readMenuBlurAlpha
     val blurAlpha = menuConfig.readMenuBlurAlpha
-    val containerColor = colors.background.copy(
+    val surfaceColor = readMenuTintColor(menuConfig) ?: colors.background
+    val containerColor = surfaceColor.copy(
         alpha = (blurAlpha.coerceIn(0, 100) / 100f).coerceAtMost(0.6f)
         alpha = (blurAlpha.coerceIn(0, 100) / 100f).coerceAtMost(0.6f)
     )
     )
-    val topBarSurfaceBrush = readMenuTopBarSurfaceBrush(
-        colors = colors,
+    val topBarSurfaceBrush = readerMenuSurfaceBrush(
+        style = ReaderMenuTintStyle.Gradient,
+        placement = ReaderMenuPlacement.Top,
+        color = surfaceColor,
         alpha = containerColor.alpha,
         alpha = containerColor.alpha,
     )
     )
 
 
-    return drawBackdrop(
-        backdrop = backdrop!!,
-        shape = { shape },
-        effects = {
-            vibrancy()
-            blur(resolvedBlurRadius.coerceAtLeast(0.dp).toPx())
-            if (useLens) {
-                val lensRadius = menuConfig.readMenuLensRadius
-                lens(lensRadius.dp.toPx(), lensRadius.dp.toPx())
-            }
-        },
-        highlight = {
-            Highlight.Default
-        },
-        shadow = null,
-        layerBlock = if (interactiveHighlight != null) {
-            {
-                val width = size.width
-                val height = size.height
-                if (width > 0f && height > 0f) {
-                    val progress = interactiveHighlight.pressProgress
-                    val scale = 1f + 4.dp.toPx() / height * progress
-                    val maxOffset = size.minDimension
-                    val initialDerivative = 0.05f
-                    val offset = interactiveHighlight.offset
-                    translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset)
-                    translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset)
-
-                    val maxDragScale = 4.dp.toPx() / height
-                    val offsetAngle = atan2(offset.y, offset.x)
-                    scaleX = scale + maxDragScale *
-                            abs(cos(offsetAngle) * offset.x / size.maxDimension) *
-                            (width / height).coerceAtMost(1f)
-                    scaleY = scale + maxDragScale *
-                            abs(sin(offsetAngle) * offset.y / size.maxDimension) *
-                            (height / width).coerceAtMost(1f)
-                }
-            }
-        } else {
-            null
-        },
-        onDrawSurface = {
-            if (useTopBarStyle) {
-                drawRect(topBarSurfaceBrush)
-            } else {
-                drawRect(containerColor)
-            }
-        },
+    val surfaceBrush = if (useTopBarStyle) {
+        topBarSurfaceBrush
+    } else {
+        readerMenuSurfaceBrush(
+            style = ReaderMenuTintStyle.Fill,
+            placement = ReaderMenuPlacement.Bottom,
+            color = surfaceColor,
+            alpha = containerColor.alpha,
+        )
+    }
+    return readerMenuLiquidGlass(
+        backdrop = backdrop,
+        shape = shape,
+        surfaceBrush = surfaceBrush,
+        blurRadius = resolvedBlurRadius,
+        lensRadius = menuConfig.readMenuLensRadius.dp,
+        useLens = useLens,
+        interactive = interactive,
     )
     )
-        .then(interactiveHighlight?.modifier ?: Modifier)
-        .then(interactiveHighlight?.gestureModifier ?: Modifier)
 }
 }
 
 
-@OptIn(ExperimentalHazeMaterialsApi::class)
 @Composable
 @Composable
 private fun Modifier.readMenuBottomBarHazeEffect(
 private fun Modifier.readMenuBottomBarHazeEffect(
     state: HazeState,
     state: HazeState,
     colors: ReadMenuColors,
     colors: ReadMenuColors,
     shape: Shape,
     shape: Shape,
     menuConfig: ReadMenuConfig,
     menuConfig: ReadMenuConfig,
-    progressive: Boolean,
+    visualState: ReaderMenuVisualState,
     blurRadiusDp: Int? = null,
     blurRadiusDp: Int? = null,
 ): Modifier {
 ): Modifier {
-    val surfaceAlpha = menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
-    val resolvedBlurTintColor = menuConfig.readMenuBlurColorNight
-        .takeIf { it != 0 && ReadStyleResolver.isNightTheme() }
-        ?.let { Color(it) }
-        ?: menuConfig.readMenuBlurColor
-            .takeIf { it != 0 && !ReadStyleResolver.isNightTheme() }
-            ?.let { Color(it) }
-            ?: menuConfig.readMenuBlurColor
-                .takeIf { it != 0 }
-                ?.let { Color(it) }
-    val backgroundModifier = if (progressive) {
-        Modifier.background(
-            readMenuBottomBarSurfaceBrush(
-                colors = colors,
-                alpha = surfaceAlpha,
-                blurTintColor = resolvedBlurTintColor,
-            )
-        )
-    } else {
-        Modifier
-    }
     return clip(shape)
     return clip(shape)
-        .then(backgroundModifier)
-        .readMenuHazeEffect(
+        .readerMenuHazeEffect(
             state = state,
             state = state,
-            colors = colors,
-            menuConfig = menuConfig,
-            progressive = progressive,
-            progressiveBottomToTop = progressive,
-            blurRadiusDp = blurRadiusDp,
+            visualState = visualState,
+            placement = ReaderMenuPlacement.Bottom,
+            baseColor = colors.background,
+            tintColor = readMenuTintColor(menuConfig),
+            blurRadius = blurRadiusDp ?: menuConfig.readMenuBlurRadius,
+            surfaceAlpha = menuConfig.readMenuBlurAlpha,
         )
         )
 }
 }
 
 
-@OptIn(ExperimentalHazeMaterialsApi::class)
 @Composable
 @Composable
-private fun Modifier.readMenuHazeEffect(
-    state: HazeState,
-    colors: ReadMenuColors,
-    menuConfig: ReadMenuConfig,
-    progressive: Boolean = false,
-    progressiveBottomToTop: Boolean = false,
-    blurRadiusDp: Int? = null,
-): Modifier {
-    val surfaceAlpha = menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
-    val blurTintColor = menuConfig.readMenuBlurColorNight
+private fun readMenuTintColor(menuConfig: ReadMenuConfig): Color? {
+    return menuConfig.readMenuBlurColorNight
         .takeIf { it != 0 && ReadStyleResolver.isNightTheme() }
         .takeIf { it != 0 && ReadStyleResolver.isNightTheme() }
-        ?.let { Color(it) }
+        ?.let(::Color)
         ?: menuConfig.readMenuBlurColor
         ?: menuConfig.readMenuBlurColor
             .takeIf { it != 0 && !ReadStyleResolver.isNightTheme() }
             .takeIf { it != 0 && !ReadStyleResolver.isNightTheme() }
-            ?.let { Color(it) }
-            ?: menuConfig.readMenuBlurColor
-                .takeIf { it != 0 }
-                ?.let { Color(it) }
-    val hazeContainerColor = if (progressive) {
-        (blurTintColor ?: Color.Black).copy(alpha = surfaceAlpha)
-    } else {
-        colors.background.copy(alpha = surfaceAlpha)
-    }
-    val style = HazeLegado.custom(
-        containerColor = hazeContainerColor,
-        blurRadius = blurRadiusDp ?: menuConfig.readMenuBlurRadius,
-        blurAlpha = menuConfig.readMenuBlurAlpha,
-    )
-
-    return hazeEffect(
-        state = state,
-        style = style,
-    ) {
-        this.progressive = if (progressive) {
-            HazeProgressive.verticalGradient(
-                startIntensity = if (progressiveBottomToTop) 0f else 1f,
-                endIntensity = if (progressiveBottomToTop) 1f else 0f,
-            )
-        } else {
-            null
-        }
-    }
-}
-
-@Composable
-private fun readMenuTopBarSurfaceBrush(
-    colors: ReadMenuColors,
-    alpha: Float,
-): Brush {
-    val topColor = colors.background.copy(
-        alpha = alpha.coerceIn(0f, 1f),
-    )
-    val bottomColor = colors.background.copy(
-        alpha = (alpha * 0.72f).coerceIn(0f, 1f),
-    )
-    return Brush.verticalGradient(
-        colors = listOf(topColor, bottomColor),
-    )
-}
-
-@Composable
-private fun readMenuBottomBarSurfaceBrush(
-    colors: ReadMenuColors,
-    alpha: Float,
-    blurTintColor: Color? = null,
-): Brush {
-    val baseColor = blurTintColor ?: colors.background
-    val strongColor = baseColor.copy(
-        alpha = alpha.coerceIn(0f, 1f),
-    )
-    val weakColor = baseColor.copy(
-        alpha = (alpha * 0.72f).coerceIn(0f, 1f),
-    )
-    return Brush.verticalGradient(
-        colors = listOf(weakColor, strongColor),
-    )
+            ?.let(::Color)
+        ?: menuConfig.readMenuBlurColor
+            .takeIf { it != 0 }
+            ?.let(::Color)
 }
 }
 
 
 @Composable
 @Composable
-private fun readMenuColors(): ReadMenuColors {
+private fun readMenuColors(readBarStyle: Int): ReadMenuColors {
     val themeBackground = LegadoTheme.colorScheme.surfaceContainerHigh
     val themeBackground = LegadoTheme.colorScheme.surfaceContainerHigh
     val themeContent = LegadoTheme.colorScheme.onSurface
     val themeContent = LegadoTheme.colorScheme.onSurface
-    return when (ReadConfig.readBarStyle) {
+    return when (readBarStyle) {
         1 -> ReadMenuColors(
         1 -> ReadMenuColors(
             background = themeBackground,
             background = themeBackground,
             content = themeContent,
             content = themeContent,
@@ -3325,8 +3267,8 @@ private fun BrightnessBar(
     backdrop: Backdrop? = null,
     backdrop: Backdrop? = null,
     buttonGlassEnabled: Boolean = false,
     buttonGlassEnabled: Boolean = false,
     glassThumbEnabled: Boolean = false,
     glassThumbEnabled: Boolean = false,
+    onBrightnessPreview: (Int) -> Unit,
 ) {
 ) {
-    val activity = LocalActivity.current
     var sliderValue by remember(vertical, buttonGlassEnabled) {
     var sliderValue by remember(vertical, buttonGlassEnabled) {
         mutableFloatStateOf(brightness.toFloat())
         mutableFloatStateOf(brightness.toFloat())
     }
     }
@@ -3350,14 +3292,7 @@ private fun BrightnessBar(
         val target = value.roundToInt().coerceIn(0, 100)
         val target = value.roundToInt().coerceIn(0, 100)
 
 
         //直接先改亮度,如果在这里onBrightnessChange,会ANR
         //直接先改亮度,如果在这里onBrightnessChange,会ANR
-        activity?.let { act ->
-            val lp = act.window.attributes
-            val targetBrightness = target / 100f
-            if (lp.screenBrightness != targetBrightness) {
-                lp.screenBrightness = targetBrightness
-                act.window.attributes = lp
-            }
-        }
+        onBrightnessPreview(target)
     }
     }
 
 
     fun commitSliderValue(value: Float) {
     fun commitSliderValue(value: Float) {

+ 4 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt

@@ -84,6 +84,8 @@ interface ReadBookRouteHost :
 
 
     fun closeReadBook()
     fun closeReadBook()
 
 
+    fun previewBrightness(value: Int)
+
     fun upSystemUiVisibility(
     fun upSystemUiVisibility(
         isInMultiWindow: Boolean,
         isInMultiWindow: Boolean,
         toolBarHide: Boolean,
         toolBarHide: Boolean,
@@ -491,12 +493,14 @@ fun ReadBookRouteScreen(
                 state = state,
                 state = state,
                 preferences = readPreferences,
                 preferences = readPreferences,
                 onIntent = viewModel::onIntent,
                 onIntent = viewModel::onIntent,
+                onBrightnessPreview = host::previewBrightness,
                 backdrop = menuBackdrop,
                 backdrop = menuBackdrop,
                 hazeState = if (useMenuHazeSource) menuHazeState else null,
                 hazeState = if (useMenuHazeSource) menuHazeState else null,
             )
             )
             ReadBookSearchBar(state = state, onIntent = viewModel::onIntent)
             ReadBookSearchBar(state = state, onIntent = viewModel::onIntent)
             ReadBookScreen(
             ReadBookScreen(
                 state = state,
                 state = state,
+                preferences = readPreferences,
                 onIntent = viewModel::onIntent,
                 onIntent = viewModel::onIntent,
                 onBack = { controller.closeReadBook() },
                 onBack = { controller.closeReadBook() },
                 onOpenTextSelectMenuConfig = {
                 onOpenTextSelectMenuConfig = {

+ 6 - 15
app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt

@@ -12,8 +12,7 @@ import androidx.compose.ui.res.stringResource
 import androidx.core.net.toUri
 import androidx.core.net.toUri
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.R
-import io.legado.app.data.repository.ReadSettingsRepository
-import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.ui.book.read.sheet.AiRewritePresetConfigSheet
 import io.legado.app.ui.book.read.sheet.AiRewritePresetConfigSheet
 import io.legado.app.ui.book.read.sheet.AiTextCleanSheet
 import io.legado.app.ui.book.read.sheet.AiTextCleanSheet
 import io.legado.app.ui.book.read.sheet.AiTextRewriteSheet
 import io.legado.app.ui.book.read.sheet.AiTextRewriteSheet
@@ -57,6 +56,7 @@ import kotlinx.coroutines.flow.collectLatest
 @Composable
 @Composable
 fun ReadBookScreen(
 fun ReadBookScreen(
     state: ReadBookUiState,
     state: ReadBookUiState,
+    preferences: ReadPreferences,
     onIntent: (ReadBookIntent) -> Unit,
     onIntent: (ReadBookIntent) -> Unit,
     onBack: () -> Unit,
     onBack: () -> Unit,
     onOpenTextSelectMenuConfig: () -> Unit,
     onOpenTextSelectMenuConfig: () -> Unit,
@@ -186,24 +186,15 @@ fun ReadBookScreen(
         onDismissRequest = dismissSheet,
         onDismissRequest = dismissSheet,
         onIntent = onIntent,
         onIntent = onIntent,
     )
     )
-    val fontSelectReadSettings: ReadSettingsRepository = org.koin.compose.koinInject()
-    val fontSelectPreferences by fontSelectReadSettings.preferences.collectAsStateWithLifecycle(
-        initialValue = null
-    )
-    val fontSelectFolderState = remember(fontSelectPreferences) {
-        val pref = fontSelectPreferences
-        if (pref == null) {
-            FontFolderState.Loading
-        } else {
-            FontFolderState.Loaded(pref.fontFolder.takeIf { it.isNotEmpty() }?.toUri())
-        }
+    val fontSelectFolderState = remember(preferences.fontFolder) {
+        FontFolderState.Loaded(preferences.fontFolder.takeIf { it.isNotEmpty() }?.toUri())
     }
     }
     val fontSelectSystemTypefaces = stringArrayResource(R.array.system_typefaces)
     val fontSelectSystemTypefaces = stringArrayResource(R.array.system_typefaces)
     FontSelectSheet(
     FontSelectSheet(
         show = state.activeSheet is ReadBookSheet.FontSelect,
         show = state.activeSheet is ReadBookSheet.FontSelect,
         title = stringResource(R.string.select_font),
         title = stringResource(R.string.select_font),
         folderState = fontSelectFolderState,
         folderState = fontSelectFolderState,
-        selectedFontPath = ReadBookConfig.textFont,
+        selectedFontPath = state.styleConfig.textFont,
         onDismissRequest = dismissSheet,
         onDismissRequest = dismissSheet,
         onSelectFont = { onIntent(ReadBookIntent.SelectFont(it.uri.toString())) },
         onSelectFont = { onIntent(ReadBookIntent.SelectFont(it.uri.toString())) },
         onSelectSystemTypeface = { onIntent(ReadBookIntent.SelectSystemTypeface(it)) },
         onSelectSystemTypeface = { onIntent(ReadBookIntent.SelectSystemTypeface(it)) },
@@ -214,7 +205,7 @@ fun ReadBookScreen(
         show = state.activeSheet is ReadBookSheet.TitleFontSelect,
         show = state.activeSheet is ReadBookSheet.TitleFontSelect,
         title = stringResource(R.string.read_config_title_settings),
         title = stringResource(R.string.read_config_title_settings),
         folderState = fontSelectFolderState,
         folderState = fontSelectFolderState,
-        selectedFontPath = ReadBookConfig.titleFont,
+        selectedFontPath = state.styleConfig.titleFont,
         onDismissRequest = dismissSheet,
         onDismissRequest = dismissSheet,
         onSelectFont = { onIntent(ReadBookIntent.SelectTitleFont(it.uri.toString())) },
         onSelectFont = { onIntent(ReadBookIntent.SelectTitleFont(it.uri.toString())) },
         onSelectSystemTypeface = { onIntent(ReadBookIntent.SelectTitleSystemTypeface(it)) },
         onSelectSystemTypeface = { onIntent(ReadBookIntent.SelectTitleSystemTypeface(it)) },

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

@@ -2004,6 +2004,8 @@ class ReadBookViewModel(
             textColor = dur.getTextColor(),
             textColor = dur.getTextColor(),
             textColorNight = dur.getTextColorNight(),
             textColorNight = dur.getTextColorNight(),
             textColorEInk = dur.getTextColorEInk(),
             textColorEInk = dur.getTextColorEInk(),
+            textFont = config.textFont,
+            titleFont = config.titleFont,
             pageAnim = actualConfig.getPageAnim(),
             pageAnim = actualConfig.getPageAnim(),
             pageAnimEInk = actualConfig.getPageAnimEInk(),
             pageAnimEInk = actualConfig.getPageAnimEInk(),
             shareLayout = config.shareLayout,
             shareLayout = config.shareLayout,

+ 21 - 23
app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt

@@ -632,7 +632,7 @@ private fun BottomBarTab(
                 )
                 )
             },
             },
         )
         )
-        AnimatedVisibility(visible = !floatingBottomBar && bottomBarBlurMode == ReadMenuBlurMode.Haze) {
+        AnimatedVisibility(visible = !floatingBottomBar) {
             TinyDropdownSettingItem(
             TinyDropdownSettingItem(
                 title = stringResource(R.string.read_menu_bar_blur_style),
                 title = stringResource(R.string.read_menu_bar_blur_style),
                 selectedValue = preferences.readMenuBottomBarBlurStyle.toString(),
                 selectedValue = preferences.readMenuBottomBarBlurStyle.toString(),
@@ -742,30 +742,28 @@ private fun TopBarTab(
                 )
                 )
             },
             },
         )
         )
-        AnimatedVisibility(visible = topBarBlurEnabled) {
-            TinyDropdownSettingItem(
-                title = stringResource(R.string.read_menu_bar_blur_style),
-                selectedValue = preferences.readMenuTopBarBlurStyle.toString(),
-                displayEntries = arrayOf(
-                    stringResource(R.string.read_menu_blur_style_solid),
-                    stringResource(R.string.read_menu_blur_style_progressive),
-                ),
-                entryValues = arrayOf(
-                    ReadMenuBlurStyle.Solid.toString(),
-                    ReadMenuBlurStyle.Progressive.toString(),
-                ),
-                onValueChange = {
-                    onIntent(
-                        ReadBookIntent.UpdateConfig(
-                            ConfigUpdate.MenuTopBarBlurSelection(
-                                mode = ReadMenuBlurMode.Haze,
-                                style = it.toInt(),
-                            )
+        TinyDropdownSettingItem(
+            title = stringResource(R.string.read_menu_bar_blur_style),
+            selectedValue = preferences.readMenuTopBarBlurStyle.toString(),
+            displayEntries = arrayOf(
+                stringResource(R.string.read_menu_blur_style_solid),
+                stringResource(R.string.read_menu_blur_style_progressive),
+            ),
+            entryValues = arrayOf(
+                ReadMenuBlurStyle.Solid.toString(),
+                ReadMenuBlurStyle.Progressive.toString(),
+            ),
+            onValueChange = {
+                onIntent(
+                    ReadBookIntent.UpdateConfig(
+                        ConfigUpdate.MenuTopBarBlurSelection(
+                            mode = preferences.readMenuTopBarBlurMode,
+                            style = it.toInt(),
                         )
                         )
                     )
                     )
-                },
-            )
-        }
+                )
+            },
+        )
         AnimatedVisibility(visible = topBarBlurEnabled) {
         AnimatedVisibility(visible = topBarBlurEnabled) {
             TinySwitchSettingItem(
             TinySwitchSettingItem(
                 title = stringResource(R.string.read_menu_bar_liquid_glass_buttons),
                 title = stringResource(R.string.read_menu_bar_liquid_glass_buttons),

+ 42 - 0
app/src/main/java/io/legado/app/ui/widget/components/reader/ReaderMenuEffects.kt

@@ -0,0 +1,42 @@
+package io.legado.app.ui.widget.components.reader
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.runtime.Composable
+import dev.chrisbanes.haze.HazeProgressive
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.hazeEffect
+import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
+import io.legado.app.ui.theme.hazeStyle.HazeLegado
+
+@OptIn(ExperimentalHazeMaterialsApi::class)
+@Composable
+fun Modifier.readerMenuHazeEffect(
+    state: HazeState,
+    visualState: ReaderMenuVisualState,
+    placement: ReaderMenuPlacement,
+    baseColor: Color,
+    tintColor: Color?,
+    blurRadius: Int,
+    surfaceAlpha: Int,
+): Modifier {
+    val resolvedColor = tintColor
+        .takeIf { visualState.useTint }
+        ?: baseColor
+    val style = HazeLegado.custom(
+        containerColor = resolvedColor.copy(alpha = surfaceAlpha.coerceIn(0, 100) / 100f),
+        blurRadius = blurRadius,
+        blurAlpha = surfaceAlpha,
+    )
+
+    return hazeEffect(state = state, style = style) {
+        progressive = if (visualState.isProgressiveBlur) {
+            HazeProgressive.verticalGradient(
+                startIntensity = if (placement == ReaderMenuPlacement.Bottom) 0f else 1f,
+                endIntensity = if (placement == ReaderMenuPlacement.Bottom) 1f else 0f,
+            )
+        } else {
+            null
+        }
+    }
+}

+ 92 - 0
app/src/main/java/io/legado/app/ui/widget/components/reader/ReaderMenuGlass.kt

@@ -0,0 +1,92 @@
+package io.legado.app.ui.widget.components.reader
+
+import android.os.Build
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.kyant.backdrop.Backdrop
+import com.kyant.backdrop.drawBackdrop
+import com.kyant.backdrop.effects.blur
+import com.kyant.backdrop.effects.lens
+import com.kyant.backdrop.effects.vibrancy
+import com.kyant.backdrop.highlight.Highlight
+import io.legado.app.ui.animation.InteractiveHighlight
+import kotlin.math.abs
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.math.tanh
+
+fun readerMenuLiquidGlassAvailable(backdrop: Backdrop?): Boolean {
+    return backdrop != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+}
+
+@Composable
+fun Modifier.readerMenuLiquidGlass(
+    backdrop: Backdrop?,
+    shape: Shape,
+    surfaceBrush: Brush,
+    blurRadius: Dp,
+    lensRadius: Dp,
+    useLens: Boolean,
+    interactive: Boolean = false,
+): Modifier {
+    if (!readerMenuLiquidGlassAvailable(backdrop)) return this
+
+    val animationScope = rememberCoroutineScope()
+    val interactiveHighlight = if (interactive) {
+        remember(animationScope) { InteractiveHighlight(animationScope) }
+    } else {
+        null
+    }
+
+    return drawBackdrop(
+        backdrop = backdrop!!,
+        shape = { shape },
+        effects = {
+            vibrancy()
+            blur(blurRadius.coerceAtLeast(0.dp).toPx())
+            if (useLens) {
+                lens(lensRadius.toPx(), lensRadius.toPx())
+            }
+        },
+        highlight = { Highlight.Default },
+        shadow = null,
+        layerBlock = if (interactiveHighlight != null) {
+            {
+                val width = size.width
+                val height = size.height
+                if (width > 0f && height > 0f) {
+                    val progress = interactiveHighlight.pressProgress
+                    val scale = 1f + 4.dp.toPx() / height * progress
+                    val maxOffset = size.minDimension
+                    val initialDerivative = 0.05f
+                    val dragOffset = interactiveHighlight.dragOffset
+                    translationX = maxOffset *
+                            tanh(initialDerivative * dragOffset.x / maxOffset) * progress
+                    translationY = maxOffset *
+                            tanh(initialDerivative * dragOffset.y / maxOffset) * progress
+
+                    val maxDragScale = 4.dp.toPx() / height
+                    val offsetAngle = atan2(dragOffset.y, dragOffset.x)
+                    scaleX = scale + maxDragScale *
+                            abs(cos(offsetAngle) * dragOffset.x / size.maxDimension) *
+                            (width / height).coerceAtMost(1f) * progress
+                    scaleY = scale + maxDragScale *
+                            abs(sin(offsetAngle) * dragOffset.y / size.maxDimension) *
+                            (height / width).coerceAtMost(1f) * progress
+                }
+            }
+        } else {
+            null
+        },
+        onDrawSurface = { drawRect(surfaceBrush) },
+    )
+        .then(interactiveHighlight?.modifier ?: Modifier)
+        .then(interactiveHighlight?.gestureModifier ?: Modifier)
+}

+ 41 - 0
app/src/main/java/io/legado/app/ui/widget/components/reader/ReaderMenuVisualState.kt

@@ -0,0 +1,41 @@
+package io.legado.app.ui.widget.components.reader
+
+import androidx.compose.runtime.Immutable
+
+enum class ReaderMenuEffect {
+    None,
+    Haze,
+    LiquidGlass,
+}
+
+enum class ReaderMenuTintStyle {
+    Fill,
+    Gradient,
+}
+
+/**
+ * Resolves reader-menu appearance from independent style and effect inputs.
+ *
+ * This deliberately contains no novel-, manga-, or settings-specific state so both readers can
+ * share the same visual rules while keeping their actions and navigation separate.
+ */
+@Immutable
+data class ReaderMenuVisualState(
+    val effect: ReaderMenuEffect,
+    val tintStyle: ReaderMenuTintStyle,
+    val styleEnabled: Boolean,
+    val tintAllowed: Boolean,
+    val tintFill: Boolean,
+) {
+    val isGradient: Boolean
+        get() = styleEnabled && tintStyle == ReaderMenuTintStyle.Gradient
+
+    val isProgressiveBlur: Boolean
+        get() = effect == ReaderMenuEffect.Haze && isGradient
+
+    val useContrastContent: Boolean
+        get() = isProgressiveBlur
+
+    val useTint: Boolean
+        get() = tintAllowed && (isGradient || effect != ReaderMenuEffect.None || tintFill)
+}

+ 28 - 0
app/src/main/java/io/legado/app/ui/widget/components/reader/ReaderMenuVisuals.kt

@@ -0,0 +1,28 @@
+package io.legado.app.ui.widget.components.reader
+
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+
+enum class ReaderMenuPlacement {
+    Top,
+    Bottom,
+}
+
+fun readerMenuSurfaceBrush(
+    style: ReaderMenuTintStyle,
+    placement: ReaderMenuPlacement,
+    color: Color,
+    alpha: Float,
+): Brush {
+    val opaque = color.copy(alpha = alpha.coerceIn(0f, 1f))
+    if (style == ReaderMenuTintStyle.Fill) return SolidColor(opaque)
+
+    val transparent = color.copy(alpha = 0f)
+    return Brush.verticalGradient(
+        colors = when (placement) {
+            ReaderMenuPlacement.Top -> listOf(opaque, transparent)
+            ReaderMenuPlacement.Bottom -> listOf(transparent, opaque)
+        }
+    )
+}

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

@@ -2120,17 +2120,17 @@
     <string name="read_menu_blur_mode_none">关闭</string>
     <string name="read_menu_blur_mode_none">关闭</string>
     <string name="read_menu_bar_liquid_glass_summary">为悬浮底栏应用液态玻璃效果,需要 Android 13 及以上系统</string>
     <string name="read_menu_bar_liquid_glass_summary">为悬浮底栏应用液态玻璃效果,需要 Android 13 及以上系统</string>
     <string name="read_menu_bottom_bar_liquid_glass_buttons_summary">需要 Android 13 及以上系统</string>
     <string name="read_menu_bottom_bar_liquid_glass_buttons_summary">需要 Android 13 及以上系统</string>
-    <string name="read_menu_bar_blur_style">模糊样式</string>
-    <string name="read_menu_blur_style_progressive">渐进式模糊</string>
+    <string name="read_menu_bar_blur_style">着色样式</string>
+    <string name="read_menu_blur_style_progressive">渐变</string>
     <string name="read_menu_top_bar_liquid_glass_buttons_summary">需要 Android 13 及以上系统</string>
     <string name="read_menu_top_bar_liquid_glass_buttons_summary">需要 Android 13 及以上系统</string>
     <string name="read_menu_blur_radius">模糊半径</string>
     <string name="read_menu_blur_radius">模糊半径</string>
     <string name="read_menu_blur_radius_summary">模糊半径越大,系统运行越卡顿</string>
     <string name="read_menu_blur_radius_summary">模糊半径越大,系统运行越卡顿</string>
     <string name="read_menu_blur_alpha">菜单不透明度</string>
     <string name="read_menu_blur_alpha">菜单不透明度</string>
-    <string name="read_menu_blur_color">模糊着色</string>
+    <string name="read_menu_blur_color">菜单着色</string>
     <string name="read_menu_border">边框</string>
     <string name="read_menu_border">边框</string>
     <string name="read_menu_border_width">宽度</string>
     <string name="read_menu_border_width">宽度</string>
     <string name="read_menu_border_color">颜色</string>
     <string name="read_menu_border_color">颜色</string>
-    <string name="read_menu_blur_style_solid">实心模糊</string>
+    <string name="read_menu_blur_style_solid">填充</string>
     <string name="read_menu_bar_liquid_glass_buttons">液态玻璃按钮</string>
     <string name="read_menu_bar_liquid_glass_buttons">液态玻璃按钮</string>
     <string name="read_menu_top_bar_title_capsule">标题胶囊布局</string>
     <string name="read_menu_top_bar_title_capsule">标题胶囊布局</string>
     <string name="read_menu_bar_blur">模糊效果</string>
     <string name="read_menu_bar_blur">模糊效果</string>

+ 4 - 4
app/src/main/res/values-zh-rHK/strings.xml

@@ -1896,19 +1896,19 @@
     <string name="read_end">閱讀完畢</string>
     <string name="read_end">閱讀完畢</string>
     <string name="read_menu_accent_color_summary">頂部和底部閱讀列的文字和圖標顏色</string>
     <string name="read_menu_accent_color_summary">頂部和底部閱讀列的文字和圖標顏色</string>
     <string name="read_menu_bar_blur">啟用模糊</string>
     <string name="read_menu_bar_blur">啟用模糊</string>
-    <string name="read_menu_bar_blur_style">模糊樣式</string>
+    <string name="read_menu_bar_blur_style">著色樣式</string>
     <string name="read_menu_bar_liquid_glass">液態玻璃背景</string>
     <string name="read_menu_bar_liquid_glass">液態玻璃背景</string>
     <string name="read_menu_bar_liquid_glass_buttons">液態玻璃底部按鈕</string>
     <string name="read_menu_bar_liquid_glass_buttons">液態玻璃底部按鈕</string>
     <string name="read_menu_bar_liquid_glass_summary">為懸浮底部導航欄使用液態玻璃。需要 Android 13+</string>
     <string name="read_menu_bar_liquid_glass_summary">為懸浮底部導航欄使用液態玻璃。需要 Android 13+</string>
     <string name="read_menu_bg_color_summary">頂部和底部閱讀列的背景</string>
     <string name="read_menu_bg_color_summary">頂部和底部閱讀列的背景</string>
     <string name="read_menu_blur_alpha">選單不透明度</string>
     <string name="read_menu_blur_alpha">選單不透明度</string>
-    <string name="read_menu_blur_color">模糊色調</string>
+    <string name="read_menu_blur_color">選單著色</string>
     <string name="read_menu_blur_mode">選單模糊模式</string>
     <string name="read_menu_blur_mode">選單模糊模式</string>
     <string name="read_menu_blur_mode_none">無</string>
     <string name="read_menu_blur_mode_none">無</string>
     <string name="read_menu_blur_radius">模糊半徑</string>
     <string name="read_menu_blur_radius">模糊半徑</string>
     <string name="read_menu_blur_radius_summary">較高半徑可能降低效能</string>
     <string name="read_menu_blur_radius_summary">較高半徑可能降低效能</string>
-    <string name="read_menu_blur_style_progressive">漸進式模糊</string>
-    <string name="read_menu_blur_style_solid">實心模糊</string>
+    <string name="read_menu_blur_style_progressive">漸變</string>
+    <string name="read_menu_blur_style_solid">填充</string>
     <string name="read_menu_border">邊框</string>
     <string name="read_menu_border">邊框</string>
     <string name="read_menu_border_color">顏色</string>
     <string name="read_menu_border_color">顏色</string>
     <string name="read_menu_border_width">寬度</string>
     <string name="read_menu_border_width">寬度</string>

+ 4 - 4
app/src/main/res/values-zh-rTW/strings.xml

@@ -2066,14 +2066,14 @@
     <string name="read_menu_bar_liquid_glass_buttons">液態玻璃底部按鈕</string>
     <string name="read_menu_bar_liquid_glass_buttons">液態玻璃底部按鈕</string>
     <string name="read_menu_top_bar_title_capsule">標題膠囊佈局</string>
     <string name="read_menu_top_bar_title_capsule">標題膠囊佈局</string>
     <string name="read_menu_bottom_bar_liquid_glass_buttons_summary">為底部工具列按鈕和滑桿滑塊套用液態玻璃效果。需要 Android 13+</string>
     <string name="read_menu_bottom_bar_liquid_glass_buttons_summary">為底部工具列按鈕和滑桿滑塊套用液態玻璃效果。需要 Android 13+</string>
-    <string name="read_menu_bar_blur_style">模糊樣式</string>
-    <string name="read_menu_blur_style_solid">實體模糊</string>
-    <string name="read_menu_blur_style_progressive">漸進式模糊</string>
+    <string name="read_menu_bar_blur_style">著色樣式</string>
+    <string name="read_menu_blur_style_solid">填充</string>
+    <string name="read_menu_blur_style_progressive">漸變</string>
     <string name="read_menu_top_bar_liquid_glass_buttons_summary">為頂部工具列操作按鈕套用液態玻璃效果。需要 Android 13+</string>
     <string name="read_menu_top_bar_liquid_glass_buttons_summary">為頂部工具列操作按鈕套用液態玻璃效果。需要 Android 13+</string>
     <string name="read_menu_blur_radius">模糊半徑</string>
     <string name="read_menu_blur_radius">模糊半徑</string>
     <string name="read_menu_blur_radius_summary">較大的半徑可能降低效能</string>
     <string name="read_menu_blur_radius_summary">較大的半徑可能降低效能</string>
     <string name="read_menu_blur_alpha">選單不透明度</string>
     <string name="read_menu_blur_alpha">選單不透明度</string>
-    <string name="read_menu_blur_color">模糊色調顏色</string>
+    <string name="read_menu_blur_color">選單著色</string>
     <string name="read_menu_border">邊框</string>
     <string name="read_menu_border">邊框</string>
     <string name="read_menu_border_width">寬度</string>
     <string name="read_menu_border_width">寬度</string>
     <string name="read_menu_border_color">顏色</string>
     <string name="read_menu_border_color">顏色</string>

+ 4 - 4
app/src/main/res/values/strings.xml

@@ -2131,14 +2131,14 @@
     <string name="read_menu_bar_liquid_glass_buttons">Liquid glass bottom buttons</string>
     <string name="read_menu_bar_liquid_glass_buttons">Liquid glass bottom buttons</string>
     <string name="read_menu_top_bar_title_capsule">Title capsule layout</string>
     <string name="read_menu_top_bar_title_capsule">Title capsule layout</string>
     <string name="read_menu_bottom_bar_liquid_glass_buttons_summary">Apply liquid glass to bottom bar buttons and the slider thumb. Requires Android 13+</string>
     <string name="read_menu_bottom_bar_liquid_glass_buttons_summary">Apply liquid glass to bottom bar buttons and the slider thumb. Requires Android 13+</string>
-    <string name="read_menu_bar_blur_style">Blur style</string>
-    <string name="read_menu_blur_style_solid">Solid blur</string>
-    <string name="read_menu_blur_style_progressive">Progressive blur</string>
+    <string name="read_menu_bar_blur_style">Tint style</string>
+    <string name="read_menu_blur_style_solid">Fill</string>
+    <string name="read_menu_blur_style_progressive">Gradient</string>
     <string name="read_menu_top_bar_liquid_glass_buttons_summary">Apply liquid glass to title bar action buttons. Requires Android 13+</string>
     <string name="read_menu_top_bar_liquid_glass_buttons_summary">Apply liquid glass to title bar action buttons. Requires Android 13+</string>
     <string name="read_menu_blur_radius">Blur radius</string>
     <string name="read_menu_blur_radius">Blur radius</string>
     <string name="read_menu_blur_radius_summary">Higher radius may reduce performance</string>
     <string name="read_menu_blur_radius_summary">Higher radius may reduce performance</string>
     <string name="read_menu_blur_alpha">Menu opacity</string>
     <string name="read_menu_blur_alpha">Menu opacity</string>
-    <string name="read_menu_blur_color">Blur tint color</string>
+    <string name="read_menu_blur_color">Menu tint color</string>
     <string name="read_menu_border">Border</string>
     <string name="read_menu_border">Border</string>
     <string name="read_menu_border_width">Width</string>
     <string name="read_menu_border_width">Width</string>
     <string name="read_menu_border_color">Color</string>
     <string name="read_menu_border_color">Color</string>

+ 99 - 0
app/src/test/java/io/legado/app/ui/widget/components/reader/ReaderMenuVisualStateTest.kt

@@ -0,0 +1,99 @@
+package io.legado.app.ui.widget.components.reader
+
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ReaderMenuVisualStateTest {
+
+    @Test
+    fun `gradient without blur keeps tint but not contrast content`() {
+        val state = visualState(
+            effect = ReaderMenuEffect.None,
+            tintStyle = ReaderMenuTintStyle.Gradient,
+        )
+
+        assertTrue(state.isGradient)
+        assertTrue(state.useTint)
+        assertFalse(state.isProgressiveBlur)
+        assertFalse(state.useContrastContent)
+    }
+
+    @Test
+    fun `progressive haze enables contrast content`() {
+        val state = visualState(
+            effect = ReaderMenuEffect.Haze,
+            tintStyle = ReaderMenuTintStyle.Gradient,
+        )
+
+        assertTrue(state.isProgressiveBlur)
+        assertTrue(state.useContrastContent)
+        assertTrue(state.useTint)
+    }
+
+    @Test
+    fun `fill without blur can opt out of tint`() {
+        val state = visualState(
+            effect = ReaderMenuEffect.None,
+            tintStyle = ReaderMenuTintStyle.Fill,
+            tintFill = false,
+        )
+
+        assertFalse(state.isGradient)
+        assertFalse(state.useTint)
+    }
+
+    @Test
+    fun `fill with haze uses tint`() {
+        val state = visualState(
+            effect = ReaderMenuEffect.Haze,
+            tintStyle = ReaderMenuTintStyle.Fill,
+            tintFill = false,
+        )
+
+        assertFalse(state.isProgressiveBlur)
+        assertTrue(state.useTint)
+    }
+
+    @Test
+    fun `liquid glass does not enable progressive contrast content`() {
+        val state = visualState(
+            effect = ReaderMenuEffect.LiquidGlass,
+            tintStyle = ReaderMenuTintStyle.Gradient,
+        )
+
+        assertTrue(state.isGradient)
+        assertFalse(state.isProgressiveBlur)
+        assertFalse(state.useContrastContent)
+        assertTrue(state.useTint)
+    }
+
+    @Test
+    fun `secondary surface disables style and tint`() {
+        val state = visualState(
+            effect = ReaderMenuEffect.Haze,
+            tintStyle = ReaderMenuTintStyle.Gradient,
+            styleEnabled = false,
+            tintAllowed = false,
+        )
+
+        assertFalse(state.isGradient)
+        assertFalse(state.isProgressiveBlur)
+        assertFalse(state.useContrastContent)
+        assertFalse(state.useTint)
+    }
+
+    private fun visualState(
+        effect: ReaderMenuEffect,
+        tintStyle: ReaderMenuTintStyle,
+        styleEnabled: Boolean = true,
+        tintAllowed: Boolean = true,
+        tintFill: Boolean = true,
+    ) = ReaderMenuVisualState(
+        effect = effect,
+        tintStyle = tintStyle,
+        styleEnabled = styleEnabled,
+        tintAllowed = tintAllowed,
+        tintFill = tintFill,
+    )
+}