Parcourir la source

feat: 整理代码

HapeLee il y a 3 mois
Parent
commit
7e435c1ead

+ 57 - 2711
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt

@@ -15,179 +15,82 @@ import androidx.compose.animation.slideInVertically
 import androidx.compose.animation.slideOutHorizontally
 import androidx.compose.animation.slideOutVertically
 import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.LocalIndication
 import androidx.compose.foundation.background
-import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.WindowInsetsSides
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.navigationBars
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.only
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.safeDrawing
-import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.automirrored.filled.ArrowForward
-import androidx.compose.material.icons.automirrored.filled.Login
-import androidx.compose.material.icons.filled.Animation
-import androidx.compose.material.icons.filled.AutoStories
-import androidx.compose.material.icons.filled.Block
-import androidx.compose.material.icons.filled.Bookmark
-import androidx.compose.material.icons.filled.BrightnessAuto
-import androidx.compose.material.icons.filled.BugReport
-import androidx.compose.material.icons.filled.CleanHands
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.CloudDownload
-import androidx.compose.material.icons.filled.Edit
-import androidx.compose.material.icons.filled.Extension
-import androidx.compose.material.icons.filled.FindReplace
-import androidx.compose.material.icons.filled.Image
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material.icons.filled.MoreVert
-import androidx.compose.material.icons.filled.Payment
-import androidx.compose.material.icons.filled.Refresh
-import androidx.compose.material.icons.filled.Replay
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.material.icons.filled.SwapHoriz
-import androidx.compose.material.icons.filled.SwapVert
-import androidx.compose.material.icons.filled.Sync
-import androidx.compose.material.icons.filled.Toc
-import androidx.compose.material.icons.filled.Translate
-import androidx.compose.material.icons.filled.Tune
-import androidx.compose.material3.Icon
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
-import androidx.compose.material3.VerticalDivider
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.draw.drawWithCache
-import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Outline
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.graphics.drawscope.scale
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.LocalWindowInfo
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.ProgressBarRangeInfo
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.clearAndSetSemantics
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.disabled
-import androidx.compose.ui.semantics.progressBarRangeInfo
-import androidx.compose.ui.semantics.role
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.setProgress
-import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.lerp
-import coil.compose.AsyncImage
 import com.kyant.backdrop.Backdrop
-import com.kyant.backdrop.backdrops.layerBackdrop
-import com.kyant.backdrop.backdrops.rememberBackdrop
-import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
-import com.kyant.backdrop.backdrops.rememberLayerBackdrop
-import com.kyant.backdrop.drawBackdrop
-import com.kyant.backdrop.effects.blur
-import com.kyant.backdrop.effects.lens
-import com.kyant.backdrop.highlight.Highlight
-import com.kyant.backdrop.shadow.InnerShadow
-import com.kyant.backdrop.shadow.Shadow
-import com.kyant.capsule.ContinuousCapsule
 import dev.chrisbanes.haze.HazeState
 import io.legado.app.R
 import io.legado.app.constant.ReadMenuBlurMode
-import io.legado.app.constant.ReadMenuBlurStyle
-import io.legado.app.data.entities.Book
 import io.legado.app.data.repository.ReadPreferences
-import io.legado.app.help.config.ReadStyleResolver
-import io.legado.app.ui.animation.DampedDragAnimation
 import io.legado.app.ui.book.read.sheet.AutoReadContent
 import io.legado.app.ui.book.read.sheet.ReadAloudContent
-import io.legado.app.ui.book.read.sheet.ReadMenuButtonInfo
 import io.legado.app.ui.book.read.sheet.ReadStyleContent
 import io.legado.app.ui.book.read.sheet.TypographyPage
 import io.legado.app.ui.book.read.sheet.TypographySection
-import io.legado.app.ui.book.read.sheet.readMenuButtonInfos
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.AppSlider
-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.card.TextCard
-import io.legado.app.ui.widget.components.divider.PillDivider
-import io.legado.app.ui.widget.components.icon.AppIcons
-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.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.settingItem.LocalSliderDragState
 import io.legado.app.ui.widget.components.settingItem.SliderDragState
-import io.legado.app.ui.widget.components.text.AnimatedText
-import io.legado.app.ui.widget.components.text.AppText
-import kotlin.math.ceil
 import kotlin.math.roundToInt
 
 /**
@@ -510,12 +413,7 @@ private fun ReadBookMenuSurface(
     }
 
     val bottomBarBorderWidth = state.menuConfig.readMenuBorderWidth
-    val bottomBarBorderColor = (if (ReadStyleResolver.isNightTheme()) {
-        state.menuConfig.readMenuBorderColorNight
-    } else {
-        state.menuConfig.readMenuBorderColor
-    }).takeIf { it != 0 }
-        ?: LegadoTheme.colorScheme.outlineVariant.hashCode()
+    val bottomBarBorderColor = readMenuBorderColor(state.menuConfig)
     val extendSurfaceToNavigationBar = !isFloating && !dialogLikeRoute
     val useLiquidGlass = readMenuBottomBarLiquidGlassEnabled(
         backdrop = backdrop,
@@ -563,14 +461,7 @@ private fun ReadBookMenuSurface(
         animationSpec = tween(durationMillis = 200),
         label = "ReadMenuFillAlpha",
     )
-    val bottomBarTextColor = Color(
-        if (ReadStyleResolver.isNightTheme()) {
-            state.menuConfig.readMenuTextColorNight
-        } else {
-            state.menuConfig.readMenuTextColor
-        }
-    ).takeUnless { it == Color.Unspecified || it.alpha == 0f }
-        ?: LegadoTheme.colorScheme.onSurface
+    val bottomBarTextColor = readMenuTextColor(state.menuConfig)
     val surfaceWindowInsetSides = when {
         isFloating || extendSurfaceToNavigationBar -> WindowInsetsSides.Horizontal
         else -> WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal
@@ -693,7 +584,6 @@ private fun ReadBookMenuSurface(
                         colors = colors,
                         onIntent = onIntent,
                         bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight + 16.dp else 16.dp,
-                        surfaceEffectEnabled = useLiquidGlass || useHaze,
                     )
                 }
 
@@ -706,7 +596,6 @@ private fun ReadBookMenuSurface(
                         onIntent = onIntent,
                         context = context,
                         bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight + 16.dp else 16.dp,
-                        surfaceEffectEnabled = useLiquidGlass || useHaze,
                         buttonGlassEnabled = useBottomBarButtonGlass,
                         backdrop = backdrop,
                         labelColor = bottomBarTextColor,
@@ -953,2623 +842,80 @@ private fun ReadBookMenuRoutePage(
     }
 }
 
-@Composable
-private fun MenuTitleBar(
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
-    onIntent: (ReadBookIntent) -> Unit,
+private fun readMenuBottomBarButtonLiquidGlassEnabled(
     backdrop: Backdrop?,
-    hazeState: HazeState?,
-    titleBarMode: String,
-) {
+    menuConfig: ReadMenuConfig,
+): Boolean {
+    return menuConfig.readMenuBottomBarLiquidGlassButtons &&
+            readerMenuLiquidGlassAvailable(backdrop)
+}
 
-    val topBarBorderWidth = state.menuConfig.readMenuBorderWidth
-    val topBarBorderColor = (if (ReadStyleResolver.isNightTheme()) {
-        state.menuConfig.readMenuBorderColorNight
+private fun readMenuBottomBarEffectiveBlurMode(
+    menuConfig: ReadMenuConfig,
+    isFloating: Boolean,
+): Int {
+    val mode = menuConfig.readMenuBottomBarBlurMode
+    return if (!isFloating && mode == ReadMenuBlurMode.LiquidGlass) {
+        ReadMenuBlurMode.Haze
     } else {
-        state.menuConfig.readMenuBorderColor
-    }).takeIf { it != 0 }
-        ?: LegadoTheme.colorScheme.outlineVariant.hashCode()
-    val isSliderDragging = LocalSliderDragState.current?.isDragging == true
-    val topBarSurfaceAlpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100)
-    val dragTopBarAlpha = topBarSurfaceAlpha.coerceAtMost(30)
-    val animatedTopBarAlpha by animateFloatAsState(
-        targetValue = if (isSliderDragging) dragTopBarAlpha.toFloat() else topBarSurfaceAlpha.toFloat(),
-        animationSpec = tween(durationMillis = 200),
-        label = "ReadMenuTopBarAlpha",
-    )
-    val topBarAlpha = animatedTopBarAlpha / 100f
-    val useTopBarBlur = readMenuTopBarHazeEnabled(hazeState, state.menuConfig)
-    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 = Color(
-        if (ReadStyleResolver.isNightTheme()) {
-            state.menuConfig.readMenuTextColorNight
-        } else {
-            state.menuConfig.readMenuTextColor
-        }
-    ).takeUnless { it == Color.Unspecified || it.alpha == 0f }
-        ?: LegadoTheme.colorScheme.onSurface
-    val labelStyle = LegadoTheme.typography.labelSmallEmphasized.copy(
-        shadow = androidx.compose.ui.graphics.Shadow(
-            color = Color.Black.copy(alpha = 0.12f),
-            offset = Offset.Zero,
-            blurRadius = 12f,
-        )
-    )
-
-    Column(
-        modifier = Modifier
-            .fillMaxWidth()
-            .then(
-                if (useTopBarBlur && hazeState != null) {
-                    Modifier.readerMenuHazeEffect(
-                        state = hazeState,
-                        visualState = topBarVisualState,
-                        placement = ReaderMenuPlacement.Top,
-                        baseColor = colors.background,
-                        tintColor = readMenuTintColor(state.menuConfig),
-                        blurRadius = state.menuConfig.readMenuBlurRadius,
-                        surfaceAlpha = animatedTopBarAlpha.roundToInt(),
-                    )
-                } else {
-                    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(
-                if (topBarBorderWidth > 0) {
-                    Modifier.drawBehind {
-                        val strokeWidth = topBarBorderWidth.dp.toPx()
-                        drawLine(
-                            color = Color(topBarBorderColor),
-                            start = Offset(0f, size.height),
-                            end = Offset(size.width, size.height),
-                            strokeWidth = strokeWidth,
-                        )
-                    }
-                } else Modifier
-            )
-            .windowInsetsPadding(
-                WindowInsets.safeDrawing.only(
-                    WindowInsetsSides.Top + WindowInsetsSides.Horizontal
-                )
-            )
-    ) {
-        val useTitleCapsule = readMenuTopBarTitleCapsuleEnabled(backdrop, state.menuConfig)
-        val capsuleIconColor = LegadoTheme.colorScheme.onSurfaceVariant
-
-        // Title row: left group (back + capsule/title) + right group (actions)
-        Row(
-            modifier = Modifier
-                .fillMaxWidth()
-                .padding(horizontal = 16.dp, vertical = 4.dp),
-            verticalAlignment = Alignment.CenterVertically,
-            horizontalArrangement = Arrangement.spacedBy(16.dp),
-        ) {
-            // Left group: back + capsule/title — fills remaining space
-            Row(
-                modifier = Modifier.weight(1f),
-                verticalAlignment = Alignment.CenterVertically,
-            ) {
-                MenuTitleGlassButton(
-                    onClick = { onIntent(ReadBookIntent.CloseReadBook()) },
-                    icon = Icons.AutoMirrored.Filled.ArrowBack,
-                    contentDescription = stringResource(R.string.back),
-                    state = state,
-                    colors = colors,
-                    backdrop = backdrop,
-                )
-
-                if (useTitleCapsule && titleBarMode != "1" && titleBarMode != "3") {
-                    TitleCapsuleGlassLayout(
-                        state = state,
-                        colors = colors,
-                        onIntent = onIntent,
-                        backdrop = backdrop,
-                        titleTextColor = capsuleIconColor,
-                    )
-                } else if (titleBarMode != "1" && titleBarMode != "3") {
-                    AppText(
-                        text = state.bookName,
-                        modifier = Modifier
-                            .weight(1f)
-                            .clickable { onIntent(ReadBookIntent.OpenBookInfo) }
-                            .padding(horizontal = 8.dp, vertical = 4.dp),
-                        style = LegadoTheme.typography.titleMedium.copy(
-                            shadow = androidx.compose.ui.graphics.Shadow(
-                                color = Color.Black.copy(alpha = 0.12f),
-                                offset = Offset.Zero,
-                                blurRadius = 12f
-                            )
-                        ),
-                        color = titleTextColor,
-                        maxLines = 2,
-                        overflow = TextOverflow.Ellipsis,
-                    )
-                }
-            }
-
-            // Right group: actions
-            if (readMenuTopBarButtonLiquidGlassEnabled(backdrop, state.menuConfig)) {
-                MenuTitleBarMergedGlassButton(
-                    state = state,
-                    colors = colors,
-                    onIntent = onIntent,
-                    backdrop = backdrop,
-                )
-            } else {
-                val compact = state.menuConfig.titleBarCompact
-                Row(
-                    horizontalArrangement = Arrangement.spacedBy(6.dp),
-                    verticalAlignment = Alignment.CenterVertically,
-                ) {
-                    if (!state.isLocalBook) {
-                        if (!compact && state.bookSource?.customButton == true) {
-                            SourceCustomActionButton(
-                                state = state,
-                                colors = colors,
-                                onIntent = onIntent,
-                                backdrop = backdrop,
-                            )
-                        }
-                        if (!compact) {
-                            SourceActionButton(
-                                state = state,
-                                colors = colors,
-                                onIntent = onIntent,
-                                backdrop = backdrop,
-                            )
-                            RefreshActionButton(
-                                state = state,
-                                colors = colors,
-                                onIntent = onIntent,
-                                backdrop = backdrop,
-                            )
-                            DownloadActionButton(
-                                state = state,
-                                colors = colors,
-                                onIntent = onIntent,
-                                backdrop = backdrop,
-                            )
-                        }
-                    } else if (state.isLocalBook && !compact) {
-                        if (state.isLocalTxt) {
-                            TxtTocRuleActionButton(
-                                state = state,
-                                colors = colors,
-                                onIntent = onIntent,
-                                backdrop = backdrop,
-                            )
-                        }
-                        CharsetActionButton(
-                            state = state,
-                            colors = colors,
-                            onIntent = onIntent,
-                            backdrop = backdrop,
-                        )
-                    }
-
-                    MenuTitleGlassButton(
-                        onClick = {
-                            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions))
-                        },
-                        icon = Icons.Default.MoreVert,
-                        contentDescription = stringResource(R.string.more_actions),
-                        state = state,
-                        colors = colors,
-                        backdrop = backdrop,
-                    )
-                }
-            }
-        }
-
-        // Book name on its own line (mode "1") — hidden when capsule is active
-        if (titleBarMode == "1" && !useTitleCapsule) {
-            AppText(
-                text = state.bookName,
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .clickable { onIntent(ReadBookIntent.OpenBookInfo) }
-                    .padding(horizontal = 16.dp, vertical = 4.dp),
-                style = LegadoTheme.typography.titleMedium.copy(
-                    shadow = androidx.compose.ui.graphics.Shadow(
-                        color = Color.Black.copy(alpha = 0.12f),
-                        offset = Offset.Zero,
-                        blurRadius = 12f
-                    )
-                ),
-                color = titleTextColor,
-                maxLines = 2,
-                overflow = TextOverflow.Ellipsis,
-            )
-        }
-
-        // Chapter name + source action (modes "0" and "1") — hidden when capsule is active
-        if ((titleBarMode == "0" || titleBarMode == "1") && !useTitleCapsule) {
-            Row(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(horizontal = 16.dp, vertical = 4.dp),
-                verticalAlignment = Alignment.CenterVertically,
-            ) {
-                AppText(
-                    text = state.chapterName,
-                    modifier = Modifier
-                        .weight(1f)
-                        .then(
-                            if (!state.isLocalBook) {
-                                Modifier.combinedClickable(
-                                    onClick = { onIntent(ReadBookIntent.OpenChapterUrl) },
-                                    onLongClick = {
-                                        onIntent(ReadBookIntent.ToggleReadUrlInBrowser)
-                                    },
-                                )
-                            } else {
-                                Modifier
-                            }
-                        ),
-                    style = labelStyle,
-                    color = titleTextColor,
-                    maxLines = 1,
-                    overflow = TextOverflow.Ellipsis,
-                )
-
-                if (!state.isLocalBook && state.bookSource != null) {
-                    var sourceMenuExpanded by remember { mutableStateOf(false) }
-                    Box {
-                        AppText(
-                            text = state.bookSource.bookSourceName,
-                            modifier = Modifier
-                                .clickable { sourceMenuExpanded = true }
-                                .padding(start = 8.dp),
-                            style = labelStyle,
-                            color = titleTextColor,
-                            maxLines = 1,
-                        )
-                        RoundDropdownMenu(
-                            expanded = sourceMenuExpanded,
-                            onDismissRequest = { sourceMenuExpanded = false },
-                        ) {
-                            if (!state.bookSource.loginUrl.isNullOrBlank()) {
-                                RoundDropdownMenuItem(
-                                    leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
-                                    text = stringResource(R.string.login),
-                                    onClick = {
-                                        sourceMenuExpanded = false
-                                        onIntent(ReadBookIntent.ShowLogin)
-                                    },
-                                )
-                            }
-                            if (!state.bookSource.getContentRule().payAction.isNullOrBlank()) {
-                                RoundDropdownMenuItem(
-                                    leadingIcon = { MenuItemIcon(Icons.Default.Payment) },
-                                    text = stringResource(R.string.chapter_pay),
-                                    onClick = {
-                                        sourceMenuExpanded = false
-                                        onIntent(ReadBookIntent.PayAction)
-                                    },
-                                )
-                            }
-                            RoundDropdownMenuItem(
-                                leadingIcon = { MenuItemIcon(Icons.Default.Edit) },
-                                text = stringResource(R.string.edit_source),
-                                onClick = {
-                                    sourceMenuExpanded = false
-                                    onIntent(ReadBookIntent.OpenSourceEdit)
-                                },
-                            )
-                            RoundDropdownMenuItem(
-                                leadingIcon = { MenuItemIcon(Icons.Default.Block) },
-                                text = stringResource(R.string.disable_source),
-                                onClick = {
-                                    sourceMenuExpanded = false
-                                    onIntent(ReadBookIntent.DisableSource)
-                                },
-                            )
-                        }
-                    }
-                }
-            }
-        }
-
-        Spacer(Modifier.height(4.dp))
+        mode
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun MenuTitleGlassButton(
-    onClick: () -> Unit,
-    icon: ImageVector,
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
+private fun readMenuBottomBarLiquidGlassEnabled(
     backdrop: Backdrop?,
-    modifier: Modifier = Modifier,
-    onLongClick: (() -> Unit)? = null,
-    contentDescription: String? = null,
-) {
-    ReadMenuGlassIconButton(
-        onClick = onClick,
-        icon = icon,
-        colors = colors,
-        backdrop = backdrop,
-        menuConfig = state.menuConfig,
-        glassEnabled = readMenuTopBarButtonLiquidGlassEnabled(backdrop, state.menuConfig),
-        iconStyle = state.menuConfig.titleBarIconStyle,
-        modifier = modifier,
-        onLongClick = onLongClick,
-        contentDescription = contentDescription,
-    )
+    menuConfig: ReadMenuConfig,
+    isFloating: Boolean,
+): Boolean {
+    return isFloating &&
+            readMenuBottomBarEffectiveBlurMode(
+                menuConfig,
+                isFloating
+            ) == ReadMenuBlurMode.LiquidGlass &&
+            readerMenuLiquidGlassAvailable(backdrop)
 }
 
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun ReadMenuGlassIconButton(
-    onClick: () -> Unit,
-    icon: ImageVector,
-    colors: ReadMenuColors,
-    backdrop: Backdrop?,
+private fun readMenuBottomBarHazeEnabled(
+    hazeState: HazeState?,
     menuConfig: ReadMenuConfig,
-    glassEnabled: Boolean,
-    iconStyle: Int = menuConfig.readMenuIconStyle,
-    modifier: Modifier = Modifier,
-    onLongClick: (() -> Unit)? = null,
-    selected: Boolean = false,
-    contentDescription: String? = null,
-) {
-    ReadMenuGlassButtonSurface(
-        onClick = onClick,
-        colors = colors,
-        backdrop = backdrop,
-        menuConfig = menuConfig,
-        glassEnabled = glassEnabled,
-        iconStyle = iconStyle,
-        modifier = modifier,
-        onLongClick = onLongClick,
-        selected = selected,
-        contentDescription = contentDescription,
-    ) { tint ->
-        Icon(
-            imageVector = icon,
-            contentDescription = null,
-            tint = tint,
-            modifier = Modifier.size(20.dp),
-        )
-    }
+    isFloating: Boolean,
+): Boolean {
+    return hazeState != null &&
+            readMenuBottomBarEffectiveBlurMode(menuConfig, isFloating) == ReadMenuBlurMode.Haze
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun ReadMenuGlassButtonSurface(
-    onClick: () -> Unit,
+private fun Modifier.readMenuBottomBarHazeEffect(
+    state: HazeState,
     colors: ReadMenuColors,
-    backdrop: Backdrop?,
+    shape: Shape,
     menuConfig: ReadMenuConfig,
-    glassEnabled: Boolean,
-    iconStyle: Int = menuConfig.readMenuIconStyle,
-    modifier: Modifier = Modifier,
-    onLongClick: (() -> Unit)? = null,
-    selected: Boolean = false,
-    contentDescription: String? = null,
-    content: @Composable (Color) -> Unit,
-) {
-    val shape = CircleShape
-    val tint = when {
-        selected -> LegadoTheme.colorScheme.primary
-        else -> LegadoTheme.colorScheme.onSurfaceVariant
-    }
-    val containerColor = when {
-        selected -> LegadoTheme.colorScheme.secondaryContainer
-        iconStyle == 1 -> LegadoTheme.colorScheme.surfaceContainerLow
-        else -> Color.Transparent
-    }
-    val border = when {
-        selected -> BorderStroke(1.5.dp, LegadoTheme.colorScheme.secondary)
-        !glassEnabled && iconStyle == 2 -> BorderStroke(1.dp, tint.copy(alpha = 0.45f))
-        else -> null
-    }
-    val outerSize = if (glassEnabled) 48.dp else 40.dp
-    val innerSize = 40.dp
-
-    Box(
-        contentAlignment = Alignment.Center,
-        modifier = modifier
-            .size(outerSize),
-    ) {
-        Box(
-            contentAlignment = Alignment.Center,
-            modifier = Modifier
-                .size(innerSize)
-                .then(
-                    if (glassEnabled) {
-                        Modifier.readMenuLiquidGlass(
-                            backdrop = backdrop,
-                            colors = colors,
-                            shape = shape,
-                            useTopBarStyle = true,
-                            useLens = true,
-                            blurRadius = 32.dp,
-                            interactive = true,
-                            menuConfig = menuConfig,
-                        )
-                    } else {
-                        Modifier
-                            .clip(shape)
-                            .background(containerColor, shape)
-                    }
-                )
-                .then(if (border != null) Modifier.border(border, shape) else Modifier)
-                .combinedClickable(
-                    indication = if (glassEnabled) null else LocalIndication.current,
-                    interactionSource = remember { MutableInteractionSource() },
-                    role = Role.Button,
-                    onLongClick = onLongClick,
-                    onClick = onClick,
-                )
-                .then(
-                    if (contentDescription != null) {
-                        Modifier.semantics {
-                            this.contentDescription = contentDescription
-                            role = Role.Button
-                        }
-                    } else {
-                        Modifier
-                    }
-                ),
-        ) {
-            content(tint)
-        }
-    }
+    visualState: ReaderMenuVisualState,
+    blurRadiusDp: Int? = null,
+    surfaceAlphaOverride: Int? = null,
+): Modifier {
+    return clip(shape)
+        .readerMenuHazeEffect(
+            state = state,
+            visualState = visualState,
+            placement = ReaderMenuPlacement.Bottom,
+            baseColor = colors.background,
+            tintColor = readMenuTintColor(menuConfig),
+            blurRadius = blurRadiusDp ?: menuConfig.readMenuBlurRadius,
+            surfaceAlpha = surfaceAlphaOverride ?: menuConfig.readMenuBlurAlpha,
+        )
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun RowScope.TitleCapsuleGlassLayout(
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
-    onIntent: (ReadBookIntent) -> Unit,
-    backdrop: Backdrop?,
-    titleTextColor: Color,
-) {
-    val pillShape = RoundedCornerShape(50)
-    val glassEnabled = readerMenuLiquidGlassAvailable(backdrop)
-            && state.menuConfig.readMenuTopBarLiquidGlassButtons
-    val iconStyle = state.menuConfig.titleBarIconStyle
-
-    Row(
-        modifier = Modifier
-            .weight(1f)
-            .padding(start = 12.dp)
-            .height(40.dp)
-            .then(
-                if (glassEnabled) {
-                    Modifier.readMenuLiquidGlass(
-                        backdrop = backdrop,
-                        colors = colors,
-                        shape = pillShape,
-                        useTopBarStyle = true,
-                        useLens = false,
-                        blurRadius = 32.dp,
-                        menuConfig = state.menuConfig,
-                    )
-                } else {
-                    val containerColor = when (iconStyle) {
-                        1 -> LegadoTheme.colorScheme.surfaceContainerLow
-                        else -> Color.Transparent
-                    }
-                    val border = when (iconStyle) {
-                        2 -> BorderStroke(
-                            1.dp,
-                            LegadoTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.45f)
-                        )
-
-                        else -> null
-                    }
-                    Modifier
-                        .clip(pillShape)
-                        .background(containerColor, pillShape)
-                        .then(if (border != null) Modifier.border(border, pillShape) else Modifier)
-                }
-            )
-            .then(
-                if (!state.isLocalBook) {
-                    Modifier.combinedClickable(
-                        indication = null,
-                        interactionSource = remember { MutableInteractionSource() },
-                        onClick = { onIntent(ReadBookIntent.OpenBookInfo) },
-                        onLongClick = { onIntent(ReadBookIntent.OpenChapterUrl) },
-                    )
-                } else {
-                    Modifier.clickable(
-                        indication = null,
-                        interactionSource = remember { MutableInteractionSource() },
-                    ) { onIntent(ReadBookIntent.OpenBookInfo) }
-                }
-            )
-            .padding(horizontal = 12.dp),
-        verticalAlignment = Alignment.CenterVertically,
-    ) {
-        Column(
-            modifier = Modifier.weight(1f),
-        ) {
-            AppText(
-                text = state.bookName,
-                style = LegadoTheme.typography.labelMediumEmphasized,
-                color = titleTextColor,
-                maxLines = 1,
-                overflow = TextOverflow.Ellipsis,
-            )
-            if (state.chapterName.isNotBlank()) {
-                AppText(
-                    text = state.chapterName,
-                    style = LegadoTheme.typography.labelSmall,
-                    color = titleTextColor.copy(alpha = 0.7f),
-                    maxLines = 1,
-                    overflow = TextOverflow.Ellipsis,
-                )
-            }
-        }
-    }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun MenuTitleBarMergedGlassButton(
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
-    onIntent: (ReadBookIntent) -> Unit,
-    backdrop: Backdrop?,
-) {
-    var sourceExpanded by remember { mutableStateOf(false) }
-    var refreshExpanded by remember { mutableStateOf(false) }
-
-    val pillShape = RoundedCornerShape(50)
-    val tint = LegadoTheme.colorScheme.onSurfaceVariant
-    val compact = state.menuConfig.titleBarCompact
-
-    Box {
-        Row(
-            modifier = Modifier
-                .height(40.dp)
-                .readMenuLiquidGlass(
-                    backdrop = backdrop,
-                    colors = colors,
-                    shape = pillShape,
-                    useTopBarStyle = true,
-                    useLens = true,
-                    blurRadius = 32.dp,
-                    interactive = true,
-                    menuConfig = state.menuConfig,
-                ),
-                verticalAlignment = Alignment.CenterVertically,
-            ) {
-            // SwapHoriz - change source
-            if (!state.isLocalBook && !compact) {
-                if (state.bookSource?.customButton == true) {
-                    val customButtonDescription = stringResource(R.string.custom_button)
-                    Box(
-                        contentAlignment = Alignment.Center,
-                        modifier = Modifier
-                            .size(40.dp)
-                            .combinedClickable(
-                                indication = null,
-                                interactionSource = remember { MutableInteractionSource() },
-                                role = Role.Button,
-                                onClick = { onIntent(ReadBookIntent.SourceCustomButton(false)) },
-                                onLongClick = { onIntent(ReadBookIntent.SourceCustomButton(true)) },
-                            )
-                            .semantics {
-                                contentDescription = customButtonDescription
-                                role = Role.Button
-                            },
-                    ) {
-                        Icon(
-                            imageVector = Icons.Default.Extension,
-                            contentDescription = null,
-                            tint = tint,
-                            modifier = Modifier.size(20.dp),
-                        )
-                    }
-
-                    Box(
-                        modifier = Modifier
-                            .width(1.dp)
-                            .height(20.dp)
-                            .background(tint.copy(alpha = 0.15f))
-                            .clearAndSetSemantics { }
-                    )
-                }
-
-                val changeSourceDescription = stringResource(R.string.change_origin)
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(40.dp)
-                        .combinedClickable(
-                            indication = null,
-                            interactionSource = remember { MutableInteractionSource() },
-                            role = Role.Button,
-                            onClick = { onIntent(ReadBookIntent.MenuChangeSource) },
-                            onLongClick = { sourceExpanded = true },
-                        )
-                        .semantics {
-                            contentDescription = changeSourceDescription
-                            role = Role.Button
-                        },
-                ) {
-                    Icon(
-                        imageVector = Icons.Default.SwapHoriz,
-                        contentDescription = null,
-                        tint = tint,
-                        modifier = Modifier.size(20.dp),
-                    )
-                }
-
-                Box(
-                    modifier = Modifier
-                        .width(1.dp)
-                        .height(20.dp)
-                        .background(tint.copy(alpha = 0.15f))
-                        .clearAndSetSemantics { }
-                )
-
-                // Refresh
-                val refreshDescription = stringResource(R.string.menu_refresh_dur)
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(40.dp)
-                        .combinedClickable(
-                            indication = null,
-                            interactionSource = remember { MutableInteractionSource() },
-                            role = Role.Button,
-                            onClick = { onIntent(ReadBookIntent.MenuRefreshDur) },
-                            onLongClick = { refreshExpanded = true },
-                        )
-                        .semantics {
-                            contentDescription = refreshDescription
-                            role = Role.Button
-                        },
-                ) {
-                    Icon(
-                        imageVector = Icons.Default.Refresh,
-                        contentDescription = null,
-                        tint = tint,
-                        modifier = Modifier.size(20.dp),
-                    )
-                }
-
-                Box(
-                    modifier = Modifier
-                        .width(1.dp)
-                        .height(20.dp)
-                        .background(tint.copy(alpha = 0.15f))
-                        .clearAndSetSemantics { }
-                )
-
-                // Download
-                val downloadDescription = stringResource(R.string.offline_cache)
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(40.dp)
-                        .clickable(
-                            indication = null,
-                            interactionSource = remember { MutableInteractionSource() },
-                            role = Role.Button,
-                            onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Download)) },
-                        )
-                        .semantics {
-                            contentDescription = downloadDescription
-                            role = Role.Button
-                        },
-                ) {
-                    Icon(
-                        imageVector = Icons.Default.CloudDownload,
-                        contentDescription = null,
-                        tint = tint,
-                        modifier = Modifier.size(20.dp),
-                    )
-                }
-
-                Box(
-                    modifier = Modifier
-                        .width(1.dp)
-                        .height(20.dp)
-                        .background(tint.copy(alpha = 0.15f))
-                        .clearAndSetSemantics { }
-                )
-            } else if (state.isLocalBook && !compact) {
-                // TXT directory rule
-                if (state.isLocalTxt) {
-                    val tocRuleDescription = stringResource(R.string.txt_toc_rule)
-                    Box(
-                        contentAlignment = Alignment.Center,
-                        modifier = Modifier
-                            .size(40.dp)
-                            .clickable(
-                                indication = null,
-                                interactionSource = remember { MutableInteractionSource() },
-                                role = Role.Button,
-                                onClick = { onIntent(ReadBookIntent.MenuTocRegex) },
-                            )
-                            .semantics {
-                                contentDescription = tocRuleDescription
-                                role = Role.Button
-                            },
-                    ) {
-                        Icon(
-                            imageVector = Icons.Default.Toc,
-                            contentDescription = null,
-                            tint = tint,
-                            modifier = Modifier.size(20.dp),
-                        )
-                    }
-
-                    Box(
-                        modifier = Modifier
-                            .width(1.dp)
-                            .height(20.dp)
-                            .background(tint.copy(alpha = 0.15f))
-                            .clearAndSetSemantics { }
-                    )
-                }
-
-                // Text encoding
-                val charsetDescription = stringResource(R.string.set_charset)
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier
-                        .size(40.dp)
-                        .clickable(
-                            indication = null,
-                            interactionSource = remember { MutableInteractionSource() },
-                            role = Role.Button,
-                            onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Charset)) },
-                        )
-                        .semantics {
-                            contentDescription = charsetDescription
-                            role = Role.Button
-                        },
-                ) {
-                    Icon(
-                        imageVector = Icons.Default.Translate,
-                        contentDescription = null,
-                        tint = tint,
-                        modifier = Modifier.size(20.dp),
-                    )
-                }
-
-                Box(
-                    modifier = Modifier
-                        .width(1.dp)
-                        .height(20.dp)
-                        .background(tint.copy(alpha = 0.15f))
-                        .clearAndSetSemantics { }
-                )
-            }
-
-            // MoreVert - overflow menu
-            val moreActionsDescription = stringResource(R.string.more_actions)
-            Box(
-                contentAlignment = Alignment.Center,
-                modifier = Modifier
-                    .size(40.dp)
-                    .clickable(
-                        indication = null,
-                        interactionSource = remember { MutableInteractionSource() },
-                        role = Role.Button,
-                        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions)) },
-                    )
-                    .semantics {
-                        contentDescription = moreActionsDescription
-                        role = Role.Button
-                    },
-            ) {
-                Icon(
-                    imageVector = AppIcons.MoreVert,
-                    contentDescription = null,
-                    tint = tint,
-                    modifier = Modifier.size(20.dp),
-                )
-            }
-        }
-
-        // Dropdown menus
-        if (!state.isLocalBook) {
-            RoundDropdownMenu(
-                expanded = sourceExpanded,
-                onDismissRequest = { sourceExpanded = false },
-            ) { dismiss ->
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.change_origin),
-                    onClick = { dismiss(); onIntent(ReadBookIntent.MenuBookChangeSource) },
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.chapter_change_source),
-                    onClick = { dismiss(); onIntent(ReadBookIntent.MenuChapterChangeSource) },
-                )
-            }
-
-            RoundDropdownMenu(
-                expanded = refreshExpanded,
-                onDismissRequest = { refreshExpanded = false },
-            ) { dismiss ->
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.menu_refresh_dur),
-                    onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshDur) },
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.menu_refresh_after),
-                    onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshAfter) },
-                )
-            }
-        }
-
-    }
-}
-
-@Composable
-private fun SourceCustomActionButton(
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
-    onIntent: (ReadBookIntent) -> Unit,
-    backdrop: Backdrop?,
-) {
-    MenuTitleGlassButton(
-        onClick = { onIntent(ReadBookIntent.SourceCustomButton(false)) },
-        onLongClick = { onIntent(ReadBookIntent.SourceCustomButton(true)) },
-        icon = Icons.Default.Extension,
-        contentDescription = stringResource(R.string.custom_button),
-        state = state,
-        colors = colors,
-        backdrop = backdrop,
-    )
-}
-
-@Composable
-private fun SourceActionButton(
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
-    onIntent: (ReadBookIntent) -> Unit,
-    backdrop: Backdrop?,
-) {
-    var expanded by remember { mutableStateOf(false) }
-
-    Box {
-        MenuTitleGlassButton(
-            onClick = { onIntent(ReadBookIntent.MenuChangeSource) },
-            onLongClick = { expanded = true },
-            icon = Icons.Default.SwapHoriz,
-            contentDescription = stringResource(R.string.change_origin),
-            state = state,
-            colors = colors,
-            backdrop = backdrop,
-        )
-
-        RoundDropdownMenu(
-            expanded = expanded,
-            onDismissRequest = { expanded = false },
-        ) { dismiss ->
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.change_origin),
-                onClick = { dismiss(); onIntent(ReadBookIntent.MenuBookChangeSource) },
-            )
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.chapter_change_source),
-                onClick = { dismiss(); onIntent(ReadBookIntent.MenuChapterChangeSource) },
-            )
-        }
-    }
-}
-
-@Composable
-private fun RefreshActionButton(
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
-    onIntent: (ReadBookIntent) -> Unit,
-    backdrop: Backdrop?,
-) {
-    var expanded by remember { mutableStateOf(false) }
-
-    Box {
-        MenuTitleGlassButton(
-            onClick = { onIntent(ReadBookIntent.MenuRefreshDur) },
-            onLongClick = { expanded = true },
-            icon = Icons.Default.Refresh,
-            contentDescription = stringResource(R.string.menu_refresh_dur),
-            state = state,
-            colors = colors,
-            backdrop = backdrop,
-        )
-
-        RoundDropdownMenu(
-            expanded = expanded,
-            onDismissRequest = { expanded = false },
-        ) { dismiss ->
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.menu_refresh_dur),
-                onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshDur) },
-            )
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.menu_refresh_after),
-                onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshAfter) },
-            )
-        }
-    }
-}
-
-@Composable
-private fun DownloadActionButton(
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
-    onIntent: (ReadBookIntent) -> Unit,
-    backdrop: Backdrop?,
-) {
-    MenuTitleGlassButton(
-        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Download)) },
-        icon = Icons.Default.CloudDownload,
-        contentDescription = stringResource(R.string.offline_cache),
-        state = state,
-        colors = colors,
-        backdrop = backdrop,
-    )
-}
-
-@Composable
-private fun TxtTocRuleActionButton(
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
-    onIntent: (ReadBookIntent) -> Unit,
-    backdrop: Backdrop?,
-) {
-    MenuTitleGlassButton(
-        onClick = { onIntent(ReadBookIntent.MenuTocRegex) },
-        icon = Icons.Default.Toc,
-        contentDescription = stringResource(R.string.txt_toc_rule),
-        state = state,
-        colors = colors,
-        backdrop = backdrop,
-    )
-}
-
-@Composable
-private fun CharsetActionButton(
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
-    onIntent: (ReadBookIntent) -> Unit,
-    backdrop: Backdrop?,
-) {
-    MenuTitleGlassButton(
-        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Charset)) },
-        icon = Icons.Default.Translate,
-        contentDescription = stringResource(R.string.set_charset),
-        state = state,
-        colors = colors,
-        backdrop = backdrop,
-    )
-}
-
-@Composable
-private fun FloatingIconRow(
-    state: ReadBookUiState,
-    preferences: ReadPreferences,
-    eyeProtectionActive: Boolean,
-    colors: ReadMenuColors,
-    alignment: Alignment.Horizontal = Alignment.CenterHorizontally,
-    onIntent: (ReadBookIntent) -> Unit,
-    backdrop: Backdrop?,
-) {
-    val context = LocalContext.current
-    val floatingIcons = remember(
-        state.menuConfig.titleBarButtons,
-        state.isReadAloudRunning,
-        state.isAutoPage,
-        state.translationMode,
-        state.useReplaceRule,
-        eyeProtectionActive,
-    ) {
-        loadFloatingIcons(
-            context = context,
-            state = state,
-            preferences = preferences,
-            eyeProtectionActive = eyeProtectionActive,
-            onIntent = onIntent,
-        )
-    }
-
-    if (floatingIcons.isEmpty()) return
-
-    Row(
-        modifier = Modifier
-            .fillMaxWidth()
-            .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
-            .padding(all = 16.dp),
-        horizontalArrangement = when (alignment) {
-            Alignment.Start -> Arrangement.Start
-            Alignment.End -> Arrangement.End
-            else -> Arrangement.Center
-        },
-        verticalAlignment = Alignment.CenterVertically,
-    ) {
-        floatingIcons.forEach { iconDef ->
-            val customPath = remember(state.menuConfig.titleBarCustomIcons, iconDef.id) {
-                state.menuConfig.titleBarCustomIcons[iconDef.id]
-            }
-            val isCustom = !customPath.isNullOrBlank()
-            val glassEnabled = !isCustom && state.menuConfig.readMenuFloatingIconLiquidGlass &&
-                    readerMenuLiquidGlassAvailable(backdrop)
-            ReadMenuGlassButtonSurface(
-                onClick = iconDef.onClick,
-                colors = colors,
-                backdrop = backdrop,
-                menuConfig = state.menuConfig,
-                glassEnabled = glassEnabled,
-                iconStyle = 1,
-                selected = iconDef.isActive,
-                modifier = Modifier.padding(horizontal = 4.dp),
-                onLongClick = iconDef.onLongClick,
-                contentDescription = iconDef.label,
-            ) {
-                if (isCustom) {
-                    AsyncImage(
-                        model = customPath,
-                        contentDescription = null,
-                        contentScale = ContentScale.Crop,
-                        modifier = Modifier
-                            .size(36.dp)
-                            .clip(CircleShape),
-                    )
-                } else {
-                    Icon(
-                        imageVector = iconDef.icon,
-                        contentDescription = null,
-                        tint = if (iconDef.isActive) LegadoTheme.colorScheme.primary else colors.content,
-                        modifier = Modifier.size(20.dp),
-                    )
-                }
-            }
-        }
-    }
-}
-
-@Composable
-private fun OverflowDropdownMenu(
-    state: ReadBookUiState,
-    onIntent: (ReadBookIntent) -> Unit,
-    expanded: Boolean,
-    onDismiss: () -> Unit,
-) {
-    val showIcon = state.menuConfig.showMenuIcon
-    val menuIcon: (ImageVector) -> (@Composable () -> Unit)? = { imageVector ->
-        if (showIcon) {
-            { MenuItemIcon(imageVector = imageVector) }
-        } else {
-            null
-        }
-    }
-
-    RoundDropdownMenu(
-        expanded = expanded,
-        onDismissRequest = onDismiss,
-    ) { dismiss ->
-        var imageStyleExpanded by remember { mutableStateOf(false) }
-
-        // Compact mode: moved from top bar buttons
-        if (state.menuConfig.titleBarCompact) {
-            if (!state.isLocalBook) {
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.change_origin),
-                    leadingIcon = menuIcon(Icons.Default.SwapHoriz),
-                    onClick = { dismiss(); onIntent(ReadBookIntent.MenuChangeSource) },
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.menu_refresh_dur),
-                    leadingIcon = menuIcon(Icons.Default.Refresh),
-                    onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshDur) },
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.menu_refresh_after),
-                    leadingIcon = menuIcon(Icons.Default.Refresh),
-                    onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshAfter) },
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.offline_cache),
-                    leadingIcon = menuIcon(Icons.Default.CloudDownload),
-                    onClick = { dismiss(); onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Download)) },
-                )
-            } else {
-                if (state.isLocalTxt) {
-                    RoundDropdownMenuItem(
-                        text = stringResource(R.string.txt_toc_rule),
-                        leadingIcon = menuIcon(Icons.Default.Toc),
-                        onClick = { dismiss(); onIntent(ReadBookIntent.MenuTocRegex) },
-                    )
-                }
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.set_charset),
-                    leadingIcon = menuIcon(Icons.Default.Translate),
-                    onClick = { dismiss(); onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Charset)) },
-                )
-            }
-            PillDivider()
-        }
-
-        // 内容处理
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.edit_content),
-            leadingIcon = menuIcon(Icons.Default.Edit),
-            onClick = {
-                dismiss()
-                onIntent(ReadBookIntent.OpenContentEdit)
-            },
-        )
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.bookmark_add),
-            leadingIcon = menuIcon(Icons.Default.Bookmark),
-            onClick = { dismiss(); onIntent(ReadBookIntent.AddBookmark) },
-        )
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.highlight_rule_config),
-            leadingIcon = menuIcon(Icons.Default.Tune),
-            onClick = {
-                dismiss()
-                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.HighlightRuleConfig))
-            },
-        )
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.text_processing),
-            leadingIcon = menuIcon(Icons.Default.FindReplace),
-            onClick = {
-                dismiss()
-                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing))
-            },
-        )
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.reverse_content),
-            leadingIcon = menuIcon(Icons.Default.SwapVert),
-            onClick = { dismiss(); onIntent(ReadBookIntent.MenuReverseContent) },
-        )
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.re_segment),
-            leadingIcon = menuIcon(Icons.Default.Toc),
-            isSelected = state.reSegment,
-            onClick = { onIntent(ReadBookIntent.MenuReSegment) },
-        )
-        if (state.isEpub) {
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.del_ruby_tag),
-                leadingIcon = menuIcon(Icons.Default.CleanHands),
-                isSelected = state.delRubyTag,
-                onClick = { onIntent(ReadBookIntent.MenuDelRubyTag) },
-            )
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.del_h_tag),
-                leadingIcon = menuIcon(Icons.Default.CleanHands),
-                isSelected = state.delHTag,
-                onClick = { onIntent(ReadBookIntent.MenuDelHTag) },
-            )
-        }
-
-        PillDivider()
-
-        // 阅读设置
-        Box {
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.image_style),
-                leadingIcon = menuIcon(Icons.Default.Image),
-                onClick = { imageStyleExpanded = true },
-            )
-            RoundDropdownMenu(
-                expanded = imageStyleExpanded,
-                onDismissRequest = { imageStyleExpanded = false },
-            ) { subDismiss ->
-                listOf(
-                    R.string.btn_default_s to Book.imgStyleDefault,
-                    R.string.image_style_full to Book.imgStyleFull,
-                    R.string.image_style_text to Book.imgStyleText,
-                    R.string.image_style_single to Book.imgStyleSingle,
-                ).forEach { (label, style) ->
-                    RoundDropdownMenuItem(
-                        text = stringResource(label),
-                        onClick = {
-                            subDismiss()
-                            onIntent(ReadBookIntent.MenuImageStyle(style))
-                        },
-                    )
-                }
-            }
-        }
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.book_page_anim),
-            leadingIcon = menuIcon(Icons.Default.Animation),
-            onClick = {
-                dismiss()
-                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.PageAnim))
-            },
-        )
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.simulated_reading),
-            leadingIcon = menuIcon(Icons.Default.AutoStories),
-            onClick = {
-                dismiss()
-                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.SimulatedReading))
-            },
-        )
-
-        PillDivider()
-
-        // 书源
-        if (!state.isLocalBook) {
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.menu_refresh_all),
-                leadingIcon = menuIcon(Icons.Default.Replay),
-                onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshAll) },
-            )
-            PillDivider()
-        }
-
-        // 进度同步
-        if (state.isReadingProgressSyncConfigured) {
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.get_book_progress),
-                leadingIcon = menuIcon(Icons.Default.Sync),
-                onClick = { dismiss(); onIntent(ReadBookIntent.MenuGetProgress) },
-            )
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.cover_book_progress),
-                leadingIcon = menuIcon(Icons.Default.Sync),
-                onClick = { dismiss(); onIntent(ReadBookIntent.MenuCoverProgress) },
-            )
-            PillDivider()
-        }
-
-        // 其他
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.config_btn),
-            leadingIcon = menuIcon(Icons.Default.Extension),
-            onClick = {
-                dismiss()
-                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ToolButtonConfig))
-            },
-        )
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.log),
-            leadingIcon = menuIcon(Icons.Default.BugReport),
-            onClick = {
-                dismiss()
-                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.AppLog))
-            },
-        )
-    }
-}
-
-@Composable
-private fun SearchBottomMenuContent(
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
-    onIntent: (ReadBookIntent) -> Unit,
-    bottomPadding: Dp = 0.dp,
-    surfaceEffectEnabled: Boolean = false,
-) {
-    val totalResults = state.searchResultList.size
-    val currentIndex = state.searchResultIndex.coerceIn(0, (totalResults - 1).coerceAtLeast(0))
-    val percent = if (totalResults > 0) {
-        ((currentIndex + 1) * 100 / totalResults)
-    } else {
-        0
-    }
-
-    Column(
-        modifier = Modifier
-            .fillMaxWidth()
-            .windowInsetsPadding(
-                WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)
-            )
-            .padding(top = 12.dp, bottom = bottomPadding)
-            .animateContentSize(),
-    ) {
-        Column(
-            modifier = Modifier
-                .fillMaxWidth()
-                .padding(horizontal = 16.dp),
-            verticalArrangement = Arrangement.spacedBy(8.dp),
-        ) {
-            SearchInfoPill(
-                modifier = Modifier.fillMaxWidth(),
-            ) {
-                AnimatedText(
-                    text = if (totalResults > 0) "${currentIndex + 1} / $totalResults" else "0 / 0",
-                    style = LegadoTheme.typography.labelSmallEmphasized,
-                    color = LegadoTheme.colorScheme.onSurfaceVariant,
-                    maxLines = 1,
-                )
-                Spacer(Modifier.width(8.dp))
-                TextCard(
-                    text = "$percent%"
-                )
-                Spacer(Modifier.width(16.dp))
-                VerticalDivider(
-                    color = LegadoTheme.colorScheme.outlineVariant,
-                    modifier = Modifier
-                        .height(8.dp)
-                        .width(1.dp)
-                )
-                Spacer(Modifier.width(16.dp))
-                AnimatedText(
-                    text = state.chapterName.ifBlank { "-" },
-                    style = LegadoTheme.typography.labelSmallEmphasized,
-                    color = LegadoTheme.colorScheme.onSurfaceVariant,
-                    maxLines = 1,
-                    overflow = TextOverflow.Ellipsis,
-                )
-            }
-        }
-
-        Spacer(Modifier.height(8.dp))
-
-        Row(
-            modifier = Modifier
-                .fillMaxWidth()
-                .padding(horizontal = 16.dp),
-            horizontalArrangement = Arrangement.spacedBy(8.dp),
-            verticalAlignment = Alignment.CenterVertically,
-        ) {
-            SearchMenuActionButton(
-                modifier = Modifier.weight(2f),
-                icon = Icons.Default.Search,
-                text = stringResource(R.string.all_results),
-                onClick = {
-                    onIntent(ReadBookIntent.OpenSearch(word = null, autoFocus = false))
-                },
-            )
-            SearchMenuActionButton(
-                modifier = Modifier.weight(1f),
-                icon = Icons.Default.Menu,
-                text = stringResource(R.string.main_menu),
-                onClick = {
-                    onIntent(ReadBookIntent.HideSearchMenu)
-                    onIntent(ReadBookIntent.ShowMenu)
-                },
-            )
-            SearchMenuActionButton(
-                modifier = Modifier.weight(0.55f),
-                icon = Icons.Default.Close,
-                text = null,
-                iconContentDescription = stringResource(R.string.exit),
-                onClick = { onIntent(ReadBookIntent.ExitSearch) },
-            )
-        }
-    }
-}
-
-@Composable
-private fun SearchInfoPill(
-    modifier: Modifier = Modifier,
-    content: @Composable RowScope.() -> Unit,
-) {
-    Row(
-        modifier = modifier
-            .height(40.dp)
-            .clip(RoundedCornerShape(16.dp))
-            .background(LegadoTheme.colorScheme.surfaceContainerLow)
-            .padding(horizontal = 12.dp),
-        verticalAlignment = Alignment.CenterVertically,
-        horizontalArrangement = Arrangement.Center,
-        content = content,
-    )
-}
-
-@Composable
-private fun SearchMenuActionButton(
-    modifier: Modifier = Modifier,
-    icon: ImageVector,
-    text: String?,
-    iconContentDescription: String? = null,
-    onClick: () -> Unit,
-) {
-    Row(
-        modifier = modifier
-            .height(40.dp)
-            .clip(RoundedCornerShape(16.dp))
-            .background(LegadoTheme.colorScheme.surfaceContainerLow)
-            .clickable(role = Role.Button, onClick = onClick)
-            .padding(horizontal = 12.dp),
-        horizontalArrangement = Arrangement.Center,
-        verticalAlignment = Alignment.CenterVertically,
-    ) {
-        Icon(
-            imageVector = icon,
-            contentDescription = iconContentDescription,
-            tint = LegadoTheme.colorScheme.primary,
-            modifier = Modifier.size(16.dp),
-        )
-        if (text != null) {
-            Spacer(Modifier.width(8.dp))
-            Text(
-                text = text,
-                style = LegadoTheme.typography.labelMediumEmphasized,
-                color = LegadoTheme.colorScheme.onSurfaceVariant,
-                maxLines = 1,
-                overflow = TextOverflow.Ellipsis,
-            )
-        }
-    }
-}
-
-@Composable
-private fun MenuBottomBar(
-    state: ReadBookUiState,
-    eyeProtectionEnabled: Boolean,
-    colors: ReadMenuColors,
-    onIntent: (ReadBookIntent) -> Unit,
-    context: Context,
-    bottomPadding: Dp = 0.dp,
-    surfaceEffectEnabled: Boolean = false,
-    buttonGlassEnabled: Boolean = false,
-    backdrop: Backdrop? = null,
-    labelColor: Color = LegadoTheme.colorScheme.onSurface,
-    progressBarBehavior: String,
-    onBrightnessPreview: (Int) -> Unit,
-) {
-    val seekMax = state.seekMax.coerceAtLeast(0)
-    val sliderMax = seekMax.toFloat().coerceAtLeast(1f)
-    var sliderValue by remember { mutableFloatStateOf(state.seekProgress.coerceIn(0, seekMax).toFloat()) }
-    var sliderDragging by remember { mutableStateOf(false) }
-    var previewPageIndex by remember { mutableIntStateOf(state.seekProgress.coerceIn(0, seekMax)) }
-    val toolButtonsBottomPadding = if (buttonGlassEnabled) 6.dp else 0.dp
-    val contentBottomPadding = if (bottomPadding > toolButtonsBottomPadding) {
-        bottomPadding - toolButtonsBottomPadding
-    } else {
-        0.dp
-    }
-    val progressCurrent = sliderValue.roundToInt().coerceIn(0, seekMax) + 1
-    val progressTotal = seekMax + 1
-    val progressValueDescription = stringResource(
-        if (progressBarBehavior == "page") {
-            R.string.a11y_read_progress_page
-        } else {
-            R.string.a11y_read_progress_chapter
-        },
-        progressCurrent,
-        progressTotal,
-    )
-
-    fun commitSliderValue(value: Float) {
-        val target = value.roundToInt().coerceIn(0, seekMax)
-        sliderDragging = false
-        sliderValue = target.toFloat()
-        previewPageIndex = target
-        if (progressBarBehavior == "page") {
-            onIntent(ReadBookIntent.SkipToPage(target))
-        } else {
-            onIntent(ReadBookIntent.SeekToChapter(target))
-        }
-    }
-
-    fun updateSliderValue(value: Float) {
-        val coercedValue = value.coerceIn(0f, sliderMax)
-        sliderDragging = true
-        sliderValue = coercedValue
-        if (progressBarBehavior == "page") {
-            val target = coercedValue.roundToInt().coerceIn(0, seekMax)
-            if (target != previewPageIndex) {
-                previewPageIndex = target
-                onIntent(ReadBookIntent.SkipToPage(target))
-            }
-        }
-    }
-
-    LaunchedEffect(state.seekProgress, seekMax, progressBarBehavior) {
-        val progress = state.seekProgress.coerceIn(0, seekMax)
-        previewPageIndex = progress
-        if (progressBarBehavior == "page" || !sliderDragging) {
-            sliderValue = progress.toFloat()
-        }
-    }
-
-    Column(
-        modifier = Modifier
-            .fillMaxWidth()
-            .windowInsetsPadding(
-                WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)
-            )
-            .padding(top = 8.dp, bottom = contentBottomPadding)
-            .animateContentSize(),
-    ) {
-        if (state.menuConfig.showBrightnessView == "1") {
-            BrightnessBar(
-                brightness = state.menuConfig.readBrightness,
-                onBrightnessChange = { value ->
-                    onIntent(ReadBookIntent.SetBrightness(value))
-                },
-                brightnessAuto = state.menuConfig.brightnessAuto,
-                onToggleAuto = {
-                    onIntent(ReadBookIntent.ToggleBrightnessAuto(!state.menuConfig.brightnessAuto))
-                },
-                onTogglePosition = {},
-                vertical = false,
-                colors = colors,
-                menuConfig = state.menuConfig,
-                backdrop = backdrop,
-                buttonGlassEnabled = buttonGlassEnabled,
-                glassThumbEnabled = buttonGlassEnabled,
-                onBrightnessPreview = onBrightnessPreview,
-            )
-            Spacer(Modifier.height(4.dp))
-        }
-
-        // Seek bar row: prev + slider + next
-        AnimatedVisibility(visible = state.menuConfig.readSliderMode != "1") {
-            Row(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(vertical = 4.dp)
-                    .padding(horizontal = 16.dp),
-                verticalAlignment = Alignment.CenterVertically,
-                horizontalArrangement = Arrangement.spacedBy(8.dp)
-            ) {
-                BottomBarGlassIconButton(
-                    onClick = { onIntent(ReadBookIntent.PrevChapter) },
-                    icon = Icons.AutoMirrored.Filled.ArrowBack,
-                    colors = colors,
-                    backdrop = backdrop,
-                    menuConfig = state.menuConfig,
-                    glassEnabled = buttonGlassEnabled,
-                    contentDescription = stringResource(R.string.previous_chapter),
-                )
-
-                ReadMenuSlider(
-                    value = sliderValue.coerceIn(0f, sliderMax),
-                    onValueChange = ::updateSliderValue,
-                    onValueChangeFinished = {
-                        commitSliderValue(sliderValue)
-                    },
-                    onValueCommit = ::commitSliderValue,
-                    valueRange = 0f..sliderMax,
-                    steps = (seekMax - 1).coerceAtLeast(0),
-                    enabled = seekMax > 0,
-                    backdrop = backdrop,
-                    glassThumbEnabled = buttonGlassEnabled,
-                    accessibilityLabel = stringResource(R.string.progress),
-                    accessibilityValue = progressValueDescription,
-                    modifier = Modifier
-                        .weight(1f)
-                        .padding(horizontal = 8.dp)
-                )
-
-                BottomBarGlassIconButton(
-                    onClick = { onIntent(ReadBookIntent.NextChapter) },
-                    icon = Icons.AutoMirrored.Filled.ArrowForward,
-                    colors = colors,
-                    backdrop = backdrop,
-                    menuConfig = state.menuConfig,
-                    glassEnabled = buttonGlassEnabled,
-                    contentDescription = stringResource(R.string.next_chapter),
-                )
-            }
-        }
-
-        Spacer(Modifier.height(12.dp))
-
-        // Tool buttons
-        val toolButtons = remember(
-            context,
-            state.menuConfig.bottomBarButtons,
-            state.menuConfig.readMenuCustomIcons,
-            state.isReadAloudRunning,
-            state.isAutoPage,
-            state.translationMode,
-            state.useReplaceRule,
-            eyeProtectionEnabled,
-        ) {
-            loadToolButtons(
-                context = context,
-                state = state,
-                eyeProtectionEnabled = eyeProtectionEnabled,
-                onIntent = onIntent,
-            )
-        }
-        val itemsPerRow = state.menuConfig.readMenuIconItemsPerRow
-        val rowCount = state.menuConfig.readMenuIconRowCount
-        val pageSize = (itemsPerRow * rowCount).coerceAtLeast(1)
-        val pageCount = ceil(toolButtons.size / pageSize.toFloat()).roundToInt().coerceAtLeast(1)
-        val pagerState = rememberPagerState(pageCount = { pageCount })
-
-        HorizontalPager(
-            state = pagerState,
-            modifier = Modifier
-                .fillMaxWidth(),
-        ) { page ->
-            val pageButtons = toolButtons.drop(page * pageSize).take(pageSize)
-            Column(
-                verticalArrangement = Arrangement.spacedBy(12.dp),
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(horizontal = 16.dp)
-                    .padding(bottom = toolButtonsBottomPadding),
-            ) {
-                pageButtons.chunked(itemsPerRow).forEach { rowButtons ->
-                    Row(
-                        modifier = Modifier.fillMaxWidth(),
-                        verticalAlignment = Alignment.CenterVertically,
-                        horizontalArrangement = Arrangement.SpaceBetween,
-                    ) {
-                        rowButtons.forEach { button ->
-                            ToolButtonItem(
-                                button = button,
-                                state = state,
-                                colors = colors,
-                                backdrop = backdrop,
-                                glassEnabled = buttonGlassEnabled,
-                                labelColor = labelColor,
-                                modifier = Modifier.width(if (buttonGlassEnabled) 48.dp else 40.dp),
-                            )
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun BottomBarGlassIconButton(
-    onClick: () -> Unit,
-    icon: ImageVector,
-    colors: ReadMenuColors,
-    backdrop: Backdrop?,
-    menuConfig: ReadMenuConfig,
-    glassEnabled: Boolean,
-    contentDescription: String? = null,
-) {
-    ReadMenuGlassIconButton(
-        onClick = onClick,
-        icon = icon,
-        colors = colors,
-        backdrop = backdrop,
-        menuConfig = menuConfig,
-        glassEnabled = glassEnabled,
-        contentDescription = contentDescription,
-    )
-}
-
-@Composable
-private fun ReadMenuSlider(
-    value: Float,
-    onValueChange: (Float) -> Unit,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
-    steps: Int = 0,
-    onValueChangeFinished: (() -> Unit)? = null,
-    onValueCommit: ((Float) -> Unit)? = null,
-    backdrop: Backdrop?,
-    glassThumbEnabled: Boolean,
-    accessibilityLabel: String? = null,
-    accessibilityValue: String? = null,
-) {
-    if (glassThumbEnabled && backdrop != null) {
-        ReadMenuLiquidSlider(
-            value = value,
-            onValueChange = onValueChange,
-            valueRange = valueRange,
-            steps = steps,
-            visibilityThreshold = 0.001f,
-            backdrop = backdrop,
-            modifier = modifier,
-            enabled = enabled,
-            onValueChangeFinished = onValueChangeFinished,
-            onValueCommit = onValueCommit,
-            accessibilityLabel = accessibilityLabel,
-            accessibilityValue = accessibilityValue,
-        )
-        return
-    }
-
-    val commitAction = onValueChangeFinished ?: onValueCommit?.let { commit -> { commit(value) } }
-
-    AppSlider(
-        value = value,
-        onValueChange = onValueChange,
-        modifier = modifier.padding(horizontal = 5.dp),
-        enabled = enabled,
-        valueRange = valueRange,
-        steps = steps,
-        onValueChangeFinished = commitAction,
-        accessibilityLabel = accessibilityLabel,
-        accessibilityValue = accessibilityValue,
-    )
-}
-
-@Composable
-private fun ReadMenuLiquidSlider(
-    value: Float,
-    onValueChange: (Float) -> Unit,
-    valueRange: ClosedFloatingPointRange<Float>,
-    steps: Int = 0,
-    visibilityThreshold: Float,
-    backdrop: Backdrop,
-    modifier: Modifier = Modifier,
-    enabled: Boolean = true,
-    onValueChangeFinished: (() -> Unit)? = null,
-    onValueCommit: ((Float) -> Unit)? = null,
-    accessibilityLabel: String? = null,
-    accessibilityValue: String? = null,
-) {
-    val accentColor = LegadoTheme.colorScheme.secondary
-    val trackColor = LegadoTheme.colorScheme.surfaceContainerLow
-    val thumbColor = Color.White.copy(alpha = 0.9f).compositeOver(LegadoTheme.colorScheme.surfaceContainerLow)
-    val enabledAlpha = if (enabled) 1f else 0.38f
-
-    val trackBackdrop = rememberLayerBackdrop()
-
-    BoxWithConstraints(
-        modifier
-            .fillMaxWidth()
-            .semantics {
-                accessibilityLabel?.let { contentDescription = it }
-                accessibilityValue?.let { stateDescription = it }
-                progressBarRangeInfo = ProgressBarRangeInfo(
-                    current = value.coerceIn(valueRange),
-                    range = valueRange,
-                    steps = steps,
-                )
-                if (!enabled) {
-                    disabled()
-                }
-                setProgress { target ->
-                    if (!enabled) {
-                        false
-                    } else {
-                        val nextValue = target.coerceIn(valueRange)
-                        onValueChange(nextValue)
-                        onValueCommit?.invoke(nextValue) ?: onValueChangeFinished?.invoke()
-                        true
-                    }
-                }
-            },
-        contentAlignment = Alignment.CenterStart,
-    ) {
-        val trackWidth = constraints.maxWidth
-        val rangeStart = valueRange.start
-        val rangeEnd = valueRange.endInclusive
-        val range = rangeEnd - rangeStart
-        val animationScope = rememberCoroutineScope()
-        val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
-        val dampedDragAnimation = remember(animationScope, trackWidth, rangeStart, rangeEnd, isLtr) {
-            DampedDragAnimation(
-                animationScope = animationScope,
-                initialValue = value,
-                valueRange = valueRange,
-                visibilityThreshold = visibilityThreshold,
-                initialScale = 1f,
-                pressedScale = 1.5f,
-                onDragStarted = {},
-                onDragStopped = {
-                    onValueChange(targetValue)
-                    onValueCommit?.invoke(targetValue) ?: onValueChangeFinished?.invoke()
-                },
-                onDrag = { _, dragAmount ->
-                    val delta = range * (dragAmount.x / trackWidth)
-                    val nextValue = if (isLtr) {
-                        (targetValue + delta).coerceIn(valueRange)
-                    } else {
-                        (targetValue - delta).coerceIn(valueRange)
-                    }
-                    updateValue(nextValue)
-                    onValueChange(nextValue)
-                },
-            )
-        }
-
-        LaunchedEffect(dampedDragAnimation, value) {
-            if (dampedDragAnimation.targetValue != value) {
-                dampedDragAnimation.updateValue(value)
-            }
-        }
-
-        val progress = if (range == 0f) {
-            0f
-        } else {
-            ((dampedDragAnimation.value - rangeStart) / range).coerceIn(0f, 1f)
-        }
-
-        Box(Modifier.layerBackdrop(trackBackdrop)) {
-            Box(
-                Modifier
-                    .drawBackdrop(
-                        backdrop = backdrop,
-                        shape = { ContinuousCapsule },
-                        effects = {},
-                        highlight = null,
-                        shadow = {
-                            Shadow(
-                                radius = 8.dp,
-                                color = Color.Black.copy(alpha = 0.12f),
-                            )
-                        },
-                        innerShadow = null,
-                        onDrawSurface = {
-                            drawRect(trackColor.copy(alpha = enabledAlpha))
-                        },
-                    )
-                    .pointerInput(enabled, animationScope, isLtr, trackWidth) {
-                        if (!enabled) return@pointerInput
-                        detectTapGestures { position ->
-                            val delta = range * (position.x / trackWidth)
-                            val targetValue =
-                                (if (isLtr) rangeStart + delta else rangeEnd - delta)
-                                    .coerceIn(valueRange)
-                            dampedDragAnimation.animateToValue(targetValue)
-                            onValueChange(targetValue)
-                            onValueCommit?.invoke(targetValue) ?: onValueChangeFinished?.invoke()
-                        }
-                    }
-                    .height(6f.dp)
-                    .fillMaxWidth(),
-            )
-            Box(
-                Modifier
-                    .clip(ContinuousCapsule)
-                    .background(accentColor.copy(alpha = enabledAlpha))
-                    .height(6f.dp)
-                    .layout { measurable, constraints ->
-                        val placeable = measurable.measure(constraints)
-                        val width = (constraints.maxWidth * progress).roundToInt()
-                        layout(width, placeable.height) {
-                            placeable.place(0, 0)
-                        }
-                    },
-            )
-        }
-
-        Box(
-            Modifier
-                .graphicsLayer {
-                    alpha = enabledAlpha
-                    translationX =
-                        (-size.width / 2f + trackWidth * progress)
-                            .coerceIn(-size.width / 4f, trackWidth - size.width * 3f / 4f) *
-                                if (isLtr) 1f else -1f
-                }
-                .then(if (enabled) dampedDragAnimation.modifier else Modifier)
-                .drawBackdrop(
-                    backdrop = rememberCombinedBackdrop(
-                        backdrop,
-                        rememberBackdrop(trackBackdrop) { drawBackdrop ->
-                            val pressProgress = dampedDragAnimation.pressProgress
-                            val scaleX = 2f / 3f + (1f / 3f) * pressProgress
-                            scale(scaleX, pressProgress) {
-                                drawBackdrop()
-                            }
-                        },
-                    ),
-                    shape = { ContinuousCapsule },
-                    effects = {
-                        val pressProgress = dampedDragAnimation.pressProgress
-                        blur(8.dp.toPx() * (1f - pressProgress))
-                        lens(
-                            10.dp.toPx() * pressProgress,
-                            14.dp.toPx() * pressProgress,
-                            chromaticAberration = true,
-                        )
-                    },
-                    highlight = {
-                        Highlight.Ambient.copy(
-                            width = Highlight.Ambient.width / 1.5f,
-                            blurRadius = Highlight.Ambient.blurRadius / 1.5f,
-                            alpha = dampedDragAnimation.pressProgress,
-                        )
-                    },
-                    shadow = {
-                        Shadow(
-                            radius = 8.dp,
-                            color = Color.Black.copy(alpha = 0.12f),
-                        )
-                    },
-                    innerShadow = {
-                        InnerShadow(
-                            radius = 4.dp * dampedDragAnimation.pressProgress,
-                            alpha = dampedDragAnimation.pressProgress,
-                        )
-                    },
-                    layerBlock = {
-                        scaleX = dampedDragAnimation.scaleX
-                        scaleY = dampedDragAnimation.scaleY
-                        val velocity = dampedDragAnimation.velocity / 10f
-                        scaleX /= 1f - (velocity * 0.75f).coerceIn(-0.2f, 0.2f)
-                        scaleY *= 1f - (velocity * 0.25f).coerceIn(-0.2f, 0.2f)
-                    },
-                    onDrawSurface = {
-                        val pressProgress = dampedDragAnimation.pressProgress
-                        drawRect(thumbColor.copy(alpha = 1f - pressProgress))
-                    },
-                )
-                .size(40f.dp, 24f.dp),
-        )
-    }
-}
-
-@Composable
-private fun ToolButtonItem(
-    button: ToolButtonDef,
-    state: ReadBookUiState,
-    colors: ReadMenuColors,
-    backdrop: Backdrop?,
-    glassEnabled: Boolean,
-    labelColor: Color,
-    modifier: Modifier = Modifier,
-) {
-    val iconTint = if (button.isActive) LegadoTheme.colorScheme.primary else colors.content
-    val badgeCount = when (button.id) {
-        "replace_badge" -> state.effectiveReplaceCount
-        else -> 0
-    }
-    val buttonShape = RoundedCornerShape(16.dp)
-    val containerColor = when {
-        button.isActive -> LegadoTheme.colorScheme.secondaryContainer
-        state.menuConfig.readMenuIconStyle == 1 -> LegadoTheme.colorScheme.surfaceContainerLow
-        else -> Color.Transparent
-    }
-    val borderStroke = when {
-        button.isActive -> BorderStroke(1.5.dp, LegadoTheme.colorScheme.primary)
-        state.menuConfig.readMenuIconStyle == 2 -> BorderStroke(1.dp, iconTint.copy(alpha = 0.45f))
-        else -> null
-    }
-
-    Column(
-        modifier = modifier,
-        horizontalAlignment = Alignment.CenterHorizontally
-    ) {
-        if (glassEnabled) {
-            ReadMenuGlassButtonSurface(
-                onClick = button.onClick,
-                colors = colors,
-                backdrop = backdrop,
-                menuConfig = state.menuConfig,
-                glassEnabled = true,
-                selected = button.isActive,
-                onLongClick = button.onLongClick,
-                contentDescription = button.description,
-            ) { tint ->
-                ToolButtonContent(
-                    button = button,
-                    tint = if (button.isActive) iconTint else tint,
-                    badgeCount = badgeCount,
-                )
-            }
-        } else {
-            Box(
-                contentAlignment = Alignment.Center,
-                modifier = Modifier
-                    .size(40.dp)
-                    .clip(buttonShape)
-                    .background(containerColor, buttonShape)
-                    .then(
-                        if (borderStroke != null) Modifier.border(
-                            borderStroke,
-                            buttonShape
-                        ) else Modifier
-                    )
-                    .combinedClickable(
-                        indication = LocalIndication.current,
-                        interactionSource = remember { MutableInteractionSource() },
-                        role = Role.Button,
-                        onLongClick = button.onLongClick,
-                        onClick = button.onClick,
-                    )
-                    .semantics {
-                        contentDescription = button.description
-                        role = Role.Button
-                    },
-            ) {
-                ToolButtonContent(
-                    button = button,
-                    tint = iconTint,
-                    badgeCount = badgeCount,
-                )
-            }
-        }
-        if (state.menuConfig.readMenuIconShowText) {
-            Spacer(Modifier.height(2.dp))
-            Text(
-                text = button.description,
-                style = LegadoTheme.typography.labelSmall.copy(
-                    shadow = androidx.compose.ui.graphics.Shadow(
-                        color = Color.Black.copy(alpha = 0.12f),
-                        offset = Offset.Zero,
-                        blurRadius = 12f,
-                    )
-                ),
-                color = labelColor,
-                maxLines = 1,
-                modifier = Modifier.wrapContentWidth(
-                    align = Alignment.CenterHorizontally,
-                    unbounded = true,
-                ),
-            )
-        }
-    }
-}
-
-@Composable
-private fun ToolButtonContent(
-    button: ToolButtonDef,
-    tint: Color,
-    badgeCount: Int,
-) {
-    Box(
-        contentAlignment = Alignment.Center,
-        modifier = Modifier.fillMaxSize(),
-    ) {
-        if (button.customIconPath.isNullOrBlank()) {
-            Icon(
-                imageVector = button.icon,
-                contentDescription = null,
-                modifier = Modifier.size(20.dp),
-                tint = tint,
-            )
-        } else {
-            AsyncImage(
-                model = button.customIconPath,
-                contentDescription = null,
-                contentScale = ContentScale.Crop,
-                modifier = Modifier
-                    .size(36.dp)
-                    .clip(CircleShape),
-            )
-        }
-        if (badgeCount > 0) {
-            Text(
-                text = badgeCount.toString(),
-                modifier = Modifier
-                    .align(Alignment.TopEnd)
-                    .background(
-                        LegadoTheme.colorScheme.error,
-                        RoundedCornerShape(8.dp),
-                    )
-                    .padding(horizontal = 4.dp, vertical = 1.dp),
-                style = LegadoTheme.typography.labelSmall,
-                color = LegadoTheme.colorScheme.onError,
-            )
-        }
-    }
-}
-
-private data class ToolButtonDef(
-    val id: String,
-    val icon: ImageVector,
-    val description: String,
-    val customIconPath: String?,
-    val isActive: Boolean = false,
-    val onClick: () -> Unit,
-    val onLongClick: (() -> Unit)? = null,
-)
-
-private fun loadToolButtons(
-    context: Context,
-    state: ReadBookUiState,
-    eyeProtectionEnabled: Boolean,
-    onIntent: (ReadBookIntent) -> Unit,
-): List<ToolButtonDef> {
-    val customIcons = state.menuConfig.readMenuCustomIcons
-    fun ReadMenuButtonInfo.toButton(
-        isActive: Boolean = false,
-        onLongClick: (() -> Unit)? = null,
-        onClick: () -> Unit,
-    ): ToolButtonDef {
-        return ToolButtonDef(id, icon, label, customIcons[id], isActive, onClick, onLongClick)
-    }
-    val infoMap = readMenuButtonInfos(context).associateBy { it.id }
-    val allButtons = listOf(
-        infoMap.getValue("search").toButton {
-            onIntent(ReadBookIntent.OpenSearch(null))
-        },
-        infoMap.getValue("catalog").toButton {
-            onIntent(ReadBookIntent.OpenChapterList)
-        },
-        infoMap.getValue("read_aloud").toButton(
-            isActive = state.isReadAloudRunning,
-            onLongClick = {
-                onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadAloud))
-            },
-        ) {
-            if (state.isReadAloudRunning) {
-                onIntent(ReadBookIntent.ReadAloudAction)
-            } else {
-                onIntent(ReadBookIntent.ToggleReadAloud)
-                onIntent(ReadBookIntent.HideMenu)
-            }
-        },
-        infoMap.getValue("setting").toButton {
-            onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadStyle))
-        },
-        infoMap.getValue("addBookmark").toButton {
-            onIntent(ReadBookIntent.AddBookmark)
-        },
-        infoMap.getValue("theme").toButton {
-            onIntent(ReadBookIntent.ToggleDayNight)
-        },
-        infoMap.getValue("eye_protection").toButton(
-            isActive = eyeProtectionEnabled,
-            onLongClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.EyeProtection)) },
-        ) {
-            onIntent(ReadBookIntent.ToggleEyeProtection)
-        },
-        infoMap.getValue("prev_chapter").toButton {
-            onIntent(ReadBookIntent.PrevChapter)
-        },
-        infoMap.getValue("next_chapter").toButton {
-            onIntent(ReadBookIntent.NextChapter)
-        },
-        infoMap.getValue("replace").toButton(
-        ) {
-            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing))
-        },
-        infoMap.getValue("replace_badge").toButton {
-            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing))
-        },
-        infoMap.getValue("auto_page").toButton(isActive = state.isAutoPage) {
-            if (state.isAutoPage) {
-                onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.AutoRead))
-            } else {
-                onIntent(ReadBookIntent.ToggleAutoPage)
-                onIntent(ReadBookIntent.HideMenu)
-            }
-        },
-        infoMap.getValue("translate").toButton(isActive = state.translationMode) {
-            onIntent(ReadBookIntent.ToggleTranslation)
-        },
-        infoMap.getValue("ai_summary").toButton {
-            onIntent(ReadBookIntent.OpenChapterSummary)
-        },
-        infoMap.getValue("ai_rewrite").toButton {
-            onIntent(ReadBookIntent.OpenAiCurrentChapterRewrite)
-        },
-        infoMap.getValue("more_actions").toButton {
-            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions))
-        },
-    )
-
-    val allMap = allButtons.associateBy { it.id }
-    return state.menuConfig.bottomBarButtons
-        .asSequence()
-        .filter { it.enabled }
-        .mapNotNull { allMap[it.id] }
-        .toList()
-}
-
-private data class ReadMenuColors(
-    val background: Color,
-    val content: Color,
-)
-
-private fun Int.toReaderMenuTintStyle(): ReaderMenuTintStyle {
-    return if (this == ReadMenuBlurStyle.Progressive) {
-        ReaderMenuTintStyle.Gradient
-    } else {
-        ReaderMenuTintStyle.Fill
-    }
-}
-
-private fun readMenuTopBarButtonLiquidGlassEnabled(
-    backdrop: Backdrop?,
-    menuConfig: ReadMenuConfig,
-): Boolean {
-    return menuConfig.readMenuTopBarLiquidGlassButtons &&
-            readerMenuLiquidGlassAvailable(backdrop)
-}
-
-private fun readMenuTopBarTitleCapsuleEnabled(
-    backdrop: Backdrop?,
-    menuConfig: ReadMenuConfig,
-): Boolean {
-    return menuConfig.readMenuTopBarTitleCapsule
-}
-
-private fun readMenuBottomBarButtonLiquidGlassEnabled(
-    backdrop: Backdrop?,
-    menuConfig: ReadMenuConfig,
-): Boolean {
-    return menuConfig.readMenuBottomBarLiquidGlassButtons &&
-            readerMenuLiquidGlassAvailable(backdrop)
-}
-
-private fun readMenuTopBarHazeEnabled(
-    hazeState: HazeState?,
-    menuConfig: ReadMenuConfig,
-): Boolean {
-    return hazeState != null && menuConfig.readMenuTopBarBlurMode == ReadMenuBlurMode.Haze
-}
-
-private fun readMenuBottomBarEffectiveBlurMode(
-    menuConfig: ReadMenuConfig,
-    isFloating: Boolean,
-): Int {
-    val mode = menuConfig.readMenuBottomBarBlurMode
-    return if (!isFloating && mode == ReadMenuBlurMode.LiquidGlass) {
-        ReadMenuBlurMode.Haze
-    } else {
-        mode
-    }
-}
-
-private fun readMenuBottomBarLiquidGlassEnabled(
-    backdrop: Backdrop?,
-    menuConfig: ReadMenuConfig,
-    isFloating: Boolean,
-): Boolean {
-    return isFloating &&
-            readMenuBottomBarEffectiveBlurMode(
-                menuConfig,
-                isFloating
-            ) == ReadMenuBlurMode.LiquidGlass &&
-            readerMenuLiquidGlassAvailable(backdrop)
-}
-
-private fun readMenuBottomBarHazeEnabled(
-    hazeState: HazeState?,
-    menuConfig: ReadMenuConfig,
-    isFloating: Boolean,
-): Boolean {
-    return hazeState != null &&
-            readMenuBottomBarEffectiveBlurMode(menuConfig, isFloating) == ReadMenuBlurMode.Haze
-}
-
-@Composable
-private fun Modifier.readMenuLiquidGlass(
-    backdrop: Backdrop?,
-    colors: ReadMenuColors,
-    shape: Shape,
-    useTopBarStyle: Boolean,
-    useLens: Boolean,
-    blurRadius: Dp? = null,
-    interactive: Boolean = false,
-    surfaceAlphaOverride: Int? = null,
-    menuConfig: ReadMenuConfig,
-): Modifier {
-    val resolvedBlurRadius = blurRadius ?: menuConfig.readMenuBlurRadius.dp
-    val blurAlpha = surfaceAlphaOverride ?: menuConfig.readMenuBlurAlpha
-    val surfaceColor = readMenuTintColor(menuConfig) ?: colors.background
-    val containerColor = surfaceColor.copy(
-        alpha = (blurAlpha.coerceIn(0, 100) / 100f).coerceAtMost(0.6f)
-    )
-    val topBarSurfaceBrush = readerMenuSurfaceBrush(
-        style = ReaderMenuTintStyle.Gradient,
-        placement = ReaderMenuPlacement.Top,
-        color = surfaceColor,
-        alpha = containerColor.alpha,
-    )
-
-    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,
-    )
-}
-
-@Composable
-private fun Modifier.readMenuBottomBarHazeEffect(
-    state: HazeState,
-    colors: ReadMenuColors,
-    shape: Shape,
-    menuConfig: ReadMenuConfig,
-    visualState: ReaderMenuVisualState,
-    blurRadiusDp: Int? = null,
-    surfaceAlphaOverride: Int? = null,
-): Modifier {
-    return clip(shape)
-        .readerMenuHazeEffect(
-            state = state,
-            visualState = visualState,
-            placement = ReaderMenuPlacement.Bottom,
-            baseColor = colors.background,
-            tintColor = readMenuTintColor(menuConfig),
-            blurRadius = blurRadiusDp ?: menuConfig.readMenuBlurRadius,
-            surfaceAlpha = surfaceAlphaOverride ?: menuConfig.readMenuBlurAlpha,
-        )
-}
-
-@Composable
-private fun readMenuTintColor(menuConfig: ReadMenuConfig): Color? {
-    return menuConfig.readMenuBlurColorNight
-        .takeIf { it != 0 && ReadStyleResolver.isNightTheme() }
-        ?.let(::Color)
-        ?: menuConfig.readMenuBlurColor
-            .takeIf { it != 0 && !ReadStyleResolver.isNightTheme() }
-            ?.let(::Color)
-        ?: menuConfig.readMenuBlurColor
-            .takeIf { it != 0 }
-            ?.let(::Color)
-}
-
-@Composable
-private fun readMenuColors(readBarStyle: Int): ReadMenuColors {
-    val themeBackground = LegadoTheme.colorScheme.surfaceContainerHigh
-    val themeContent = LegadoTheme.colorScheme.onSurface
-    return when (readBarStyle) {
-        1 -> ReadMenuColors(
-            background = themeBackground,
-            content = themeContent,
-        )
-
-        2 -> ReadMenuColors(
-            background = LegadoTheme.colorScheme.surfaceContainerHigh,
-            content = LegadoTheme.colorScheme.primary,
-        )
+private fun readMenuColors(readBarStyle: Int): ReadMenuColors {
+    val themeBackground = LegadoTheme.colorScheme.surfaceContainerHigh
+    val themeContent = LegadoTheme.colorScheme.onSurface
+    return when (readBarStyle) {
+        2 -> ReadMenuColors(
+            background = LegadoTheme.colorScheme.surfaceContainerHigh,
+            content = LegadoTheme.colorScheme.primary,
+        )
 
         else -> ReadMenuColors(themeBackground, themeContent)
     }
 }
-
-// ========== Title Bar Icons ==========
-
-private data class FloatingIconDef(
-    val id: String,
-    val icon: ImageVector,
-    val label: String,
-    val isActive: Boolean = false,
-    val onClick: () -> Unit,
-    val onLongClick: (() -> Unit)? = null,
-)
-
-private fun loadFloatingIcons(
-    context: Context,
-    state: ReadBookUiState,
-    preferences: ReadPreferences,
-    eyeProtectionActive: Boolean,
-    onIntent: (ReadBookIntent) -> Unit,
-): List<FloatingIconDef> {
-    val infoMap = readMenuButtonInfos(context).associateBy { it.id }
-
-    val actionMap: Map<String, () -> Unit> = mapOf(
-        "search" to { onIntent(ReadBookIntent.OpenSearch(null)) },
-        "catalog" to { onIntent(ReadBookIntent.OpenChapterList) },
-        "read_aloud" to {
-            if (state.isReadAloudRunning) {
-                onIntent(ReadBookIntent.ReadAloudAction)
-            } else {
-                onIntent(ReadBookIntent.ToggleReadAloud)
-                onIntent(ReadBookIntent.HideMenu)
-            }
-        },
-        "setting" to { onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadStyle)) },
-        "addBookmark" to { onIntent(ReadBookIntent.AddBookmark) },
-        "theme" to { onIntent(ReadBookIntent.ToggleDayNight) },
-        "eye_protection" to { onIntent(ReadBookIntent.ToggleEyeProtection) },
-        "prev_chapter" to { onIntent(ReadBookIntent.PrevChapter) },
-        "next_chapter" to { onIntent(ReadBookIntent.NextChapter) },
-        "replace" to { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing)) },
-        "replace_badge" to { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing)) },
-        "auto_page" to {
-            if (state.isAutoPage) {
-                onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.AutoRead))
-            } else {
-                onIntent(ReadBookIntent.ToggleAutoPage)
-                onIntent(ReadBookIntent.HideMenu)
-            }
-        },
-        "translate" to { onIntent(ReadBookIntent.ToggleTranslation) },
-        "ai_summary" to { onIntent(ReadBookIntent.OpenChapterSummary) },
-        "ai_rewrite" to { onIntent(ReadBookIntent.OpenAiCurrentChapterRewrite) },
-        "more_actions" to { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions)) },
-    )
-
-    val activeIds = buildSet {
-        if (state.isReadAloudRunning) add("read_aloud")
-        if (state.isAutoPage) add("auto_page")
-        if (state.translationMode) add("translate")
-        if (eyeProtectionActive) add("eye_protection")
-    }
-
-    return state.menuConfig.titleBarButtons
-        .asSequence()
-        .filter { it.enabled }
-        .mapNotNull { item ->
-            val id = item.id
-            val info = infoMap[id] ?: return@mapNotNull null
-            FloatingIconDef(
-                id = id,
-                icon = info.icon,
-                label = info.label,
-                isActive = id in activeIds,
-                onClick = actionMap[id] ?: {},
-                onLongClick = when (id) {
-                    "read_aloud" -> {
-                        { onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadAloud)) }
-                    }
-
-                    else -> null
-                },
-            )
-        }
-        .toList()
-}
-
-@Composable
-private fun BrightnessBar(
-    brightness: Int,
-    onBrightnessChange: (Int) -> Unit,
-    brightnessAuto: Boolean,
-    onToggleAuto: () -> Unit,
-    onTogglePosition: () -> Unit,
-    modifier: Modifier = Modifier,
-    vertical: Boolean = false,
-    colors: ReadMenuColors,
-    menuConfig: ReadMenuConfig,
-    backdrop: Backdrop? = null,
-    buttonGlassEnabled: Boolean = false,
-    glassThumbEnabled: Boolean = false,
-    onBrightnessPreview: (Int) -> Unit,
-) {
-    var sliderValue by remember(vertical, buttonGlassEnabled) {
-        mutableFloatStateOf(brightness.toFloat())
-    }
-    var sliderDragging by remember(vertical, buttonGlassEnabled) {
-        mutableStateOf(false)
-    }
-
-    LaunchedEffect(brightness, brightnessAuto) {
-        if (brightnessAuto) {
-            sliderDragging = false
-            sliderValue = brightness.toFloat()
-        } else if (!sliderDragging) {
-            sliderValue = brightness.toFloat()
-        }
-    }
-
-    fun updateSliderValue(value: Float) {
-        if (brightnessAuto) return
-        sliderDragging = true
-        sliderValue = value.coerceIn(0f, 100f)
-        val target = value.roundToInt().coerceIn(0, 100)
-
-        //直接先改亮度,如果在这里onBrightnessChange,会ANR
-        onBrightnessPreview(target)
-    }
-
-    fun commitSliderValue(value: Float) {
-        val target = value.roundToInt().coerceIn(0, 100)
-        sliderDragging = false
-        sliderValue = target.toFloat()
-        if (brightnessAuto) return
-        onBrightnessChange(target)
-    }
-
-    fun toggleAuto() {
-        sliderDragging = false
-        sliderValue = brightness.toFloat()
-        onToggleAuto()
-    }
-
-    val brightnessValueDescription = stringResource(
-        R.string.a11y_percent_value,
-        sliderValue.roundToInt().coerceIn(0, 100),
-    )
-
-    if (vertical) {
-        Column(
-            modifier = modifier
-                .width(if (buttonGlassEnabled) 64.dp else 56.dp)
-                .padding(vertical = 12.dp, horizontal = 4.dp),
-            horizontalAlignment = Alignment.CenterHorizontally,
-            verticalArrangement = Arrangement.spacedBy(10.dp),
-        ) {
-            ReadMenuGlassIconButton(
-                onClick = ::toggleAuto,
-                icon = Icons.Filled.BrightnessAuto,
-                colors = colors,
-                backdrop = backdrop,
-                menuConfig = menuConfig,
-                glassEnabled = buttonGlassEnabled,
-                selected = brightnessAuto,
-                contentDescription = stringResource(R.string.brightness_follow_system),
-            )
-            AppVerticalSlider(
-                value = sliderValue.coerceIn(0f, 100f),
-                onValueChange = { value ->
-                    if (brightnessAuto) return@AppVerticalSlider
-                    updateSliderValue(value)
-                },
-                onValueChangeFinished = {
-                    commitSliderValue(sliderValue)
-                },
-                valueRange = 0f..100f,
-                enabled = !brightnessAuto,
-                height = 168.dp,
-                accessibilityLabel = stringResource(R.string.brightness),
-                accessibilityValue = brightnessValueDescription,
-            )
-            ReadMenuGlassIconButton(
-                onClick = onTogglePosition,
-                icon = Icons.Filled.SwapHoriz,
-                colors = colors,
-                backdrop = backdrop,
-                menuConfig = menuConfig,
-                glassEnabled = buttonGlassEnabled,
-                contentDescription = stringResource(R.string.brightness_bar_position),
-            )
-        }
-    } else {
-        val buttonSize = if (buttonGlassEnabled) 48.dp else 40.dp
-        Row(
-            modifier = modifier
-                .fillMaxWidth()
-                .padding(horizontal = 16.dp, vertical = 4.dp),
-            verticalAlignment = Alignment.CenterVertically,
-            horizontalArrangement = Arrangement.spacedBy(8.dp),
-        ) {
-            ReadMenuGlassIconButton(
-                onClick = ::toggleAuto,
-                icon = Icons.Filled.BrightnessAuto,
-                colors = colors,
-                backdrop = backdrop,
-                menuConfig = menuConfig,
-                glassEnabled = buttonGlassEnabled,
-                selected = brightnessAuto,
-                contentDescription = stringResource(R.string.brightness_follow_system),
-            )
-            ReadMenuSlider(
-                value = sliderValue.coerceIn(0f, 100f),
-                onValueChange = { value ->
-                    if (brightnessAuto) return@ReadMenuSlider
-                    updateSliderValue(value)
-                },
-                onValueChangeFinished = {
-                    commitSliderValue(sliderValue)
-                },
-                onValueCommit = ::commitSliderValue,
-                valueRange = 0f..100f,
-                enabled = !brightnessAuto,
-                backdrop = backdrop,
-                glassThumbEnabled = glassThumbEnabled,
-                accessibilityLabel = stringResource(R.string.brightness),
-                accessibilityValue = brightnessValueDescription,
-                modifier = Modifier
-                    .weight(1f)
-                    .padding(horizontal = 8.dp),
-            )
-            Spacer(Modifier.width(buttonSize))
-        }
-    }
-}

+ 436 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarBottom.kt

@@ -0,0 +1,436 @@
+package io.legado.app.ui.book.read
+
+import android.content.Context
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.filled.BrightnessAuto
+import androidx.compose.material.icons.filled.SwapHoriz
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.kyant.backdrop.Backdrop
+import io.legado.app.R
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.AppVerticalSlider
+import kotlin.math.ceil
+import kotlin.math.roundToInt
+
+@Composable
+internal fun MenuBottomBar(
+    state: ReadBookUiState,
+    eyeProtectionEnabled: Boolean,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    context: Context,
+    bottomPadding: Dp = 0.dp,
+    buttonGlassEnabled: Boolean = false,
+    backdrop: Backdrop? = null,
+    labelColor: Color = LegadoTheme.colorScheme.onSurface,
+    progressBarBehavior: String,
+    onBrightnessPreview: (Int) -> Unit,
+) {
+    val seekMax = state.seekMax.coerceAtLeast(0)
+    val sliderMax = seekMax.toFloat().coerceAtLeast(1f)
+    var sliderValue by remember {
+        mutableFloatStateOf(
+            state.seekProgress.coerceIn(0, seekMax).toFloat()
+        )
+    }
+    var sliderDragging by remember { mutableStateOf(false) }
+    var previewPageIndex by remember { mutableIntStateOf(state.seekProgress.coerceIn(0, seekMax)) }
+    val toolButtonsBottomPadding = if (buttonGlassEnabled) 6.dp else 0.dp
+    val contentBottomPadding = if (bottomPadding > toolButtonsBottomPadding) {
+        bottomPadding - toolButtonsBottomPadding
+    } else {
+        0.dp
+    }
+    val progressCurrent = sliderValue.roundToInt().coerceIn(0, seekMax) + 1
+    val progressTotal = seekMax + 1
+    val progressValueDescription = stringResource(
+        if (progressBarBehavior == "page") {
+            R.string.a11y_read_progress_page
+        } else {
+            R.string.a11y_read_progress_chapter
+        },
+        progressCurrent,
+        progressTotal,
+    )
+
+    fun commitSliderValue(value: Float) {
+        val target = value.roundToInt().coerceIn(0, seekMax)
+        sliderDragging = false
+        sliderValue = target.toFloat()
+        previewPageIndex = target
+        if (progressBarBehavior == "page") {
+            onIntent(ReadBookIntent.SkipToPage(target))
+        } else {
+            onIntent(ReadBookIntent.SeekToChapter(target))
+        }
+    }
+
+    fun updateSliderValue(value: Float) {
+        val coercedValue = value.coerceIn(0f, sliderMax)
+        sliderDragging = true
+        sliderValue = coercedValue
+        if (progressBarBehavior == "page") {
+            val target = coercedValue.roundToInt().coerceIn(0, seekMax)
+            if (target != previewPageIndex) {
+                previewPageIndex = target
+                onIntent(ReadBookIntent.SkipToPage(target))
+            }
+        }
+    }
+
+    LaunchedEffect(state.seekProgress, seekMax, progressBarBehavior) {
+        val progress = state.seekProgress.coerceIn(0, seekMax)
+        previewPageIndex = progress
+        if (progressBarBehavior == "page" || !sliderDragging) {
+            sliderValue = progress.toFloat()
+        }
+    }
+
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .windowInsetsPadding(
+                WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)
+            )
+            .padding(top = 8.dp, bottom = contentBottomPadding)
+            .animateContentSize(),
+    ) {
+        if (state.menuConfig.showBrightnessView == "1") {
+            BrightnessBar(
+                brightness = state.menuConfig.readBrightness,
+                onBrightnessChange = { value ->
+                    onIntent(ReadBookIntent.SetBrightness(value))
+                },
+                brightnessAuto = state.menuConfig.brightnessAuto,
+                onToggleAuto = {
+                    onIntent(ReadBookIntent.ToggleBrightnessAuto(!state.menuConfig.brightnessAuto))
+                },
+                onTogglePosition = {},
+                vertical = false,
+                colors = colors,
+                menuConfig = state.menuConfig,
+                backdrop = backdrop,
+                buttonGlassEnabled = buttonGlassEnabled,
+                glassThumbEnabled = buttonGlassEnabled,
+                onBrightnessPreview = onBrightnessPreview,
+            )
+            Spacer(Modifier.height(4.dp))
+        }
+
+        // Seek bar row: prev + slider + next
+        AnimatedVisibility(visible = state.menuConfig.readSliderMode != "1") {
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(vertical = 4.dp)
+                    .padding(horizontal = 16.dp),
+                verticalAlignment = Alignment.CenterVertically,
+                horizontalArrangement = Arrangement.spacedBy(8.dp)
+            ) {
+                BottomBarGlassIconButton(
+                    onClick = { onIntent(ReadBookIntent.PrevChapter) },
+                    icon = Icons.AutoMirrored.Filled.ArrowBack,
+                    colors = colors,
+                    backdrop = backdrop,
+                    menuConfig = state.menuConfig,
+                    glassEnabled = buttonGlassEnabled,
+                    contentDescription = stringResource(R.string.previous_chapter),
+                )
+
+                ReadMenuSlider(
+                    value = sliderValue.coerceIn(0f, sliderMax),
+                    onValueChange = ::updateSliderValue,
+                    onValueChangeFinished = {
+                        commitSliderValue(sliderValue)
+                    },
+                    onValueCommit = ::commitSliderValue,
+                    valueRange = 0f..sliderMax,
+                    steps = (seekMax - 1).coerceAtLeast(0),
+                    enabled = seekMax > 0,
+                    backdrop = backdrop,
+                    glassThumbEnabled = buttonGlassEnabled,
+                    accessibilityLabel = stringResource(R.string.progress),
+                    accessibilityValue = progressValueDescription,
+                    modifier = Modifier
+                        .weight(1f)
+                        .padding(horizontal = 8.dp)
+                )
+
+                BottomBarGlassIconButton(
+                    onClick = { onIntent(ReadBookIntent.NextChapter) },
+                    icon = Icons.AutoMirrored.Filled.ArrowForward,
+                    colors = colors,
+                    backdrop = backdrop,
+                    menuConfig = state.menuConfig,
+                    glassEnabled = buttonGlassEnabled,
+                    contentDescription = stringResource(R.string.next_chapter),
+                )
+            }
+        }
+
+        Spacer(Modifier.height(12.dp))
+
+        // Tool buttons
+        val toolButtons = remember(
+            context,
+            state.menuConfig.bottomBarButtons,
+            state.menuConfig.readMenuCustomIcons,
+            state.isReadAloudRunning,
+            state.isAutoPage,
+            state.translationMode,
+            state.useReplaceRule,
+            eyeProtectionEnabled,
+        ) {
+            loadToolButtons(
+                context = context,
+                state = state,
+                eyeProtectionEnabled = eyeProtectionEnabled,
+                onIntent = onIntent,
+            )
+        }
+        val itemsPerRow = state.menuConfig.readMenuIconItemsPerRow
+        val rowCount = state.menuConfig.readMenuIconRowCount
+        val pageSize = (itemsPerRow * rowCount).coerceAtLeast(1)
+        val pageCount = ceil(toolButtons.size / pageSize.toFloat()).roundToInt().coerceAtLeast(1)
+        val pagerState = rememberPagerState(pageCount = { pageCount })
+
+        HorizontalPager(
+            state = pagerState,
+            modifier = Modifier
+                .fillMaxWidth(),
+        ) { page ->
+            val pageButtons = toolButtons.drop(page * pageSize).take(pageSize)
+            Column(
+                verticalArrangement = Arrangement.spacedBy(12.dp),
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(horizontal = 16.dp)
+                    .padding(bottom = toolButtonsBottomPadding),
+            ) {
+                pageButtons.chunked(itemsPerRow).forEach { rowButtons ->
+                    Row(
+                        modifier = Modifier.fillMaxWidth(),
+                        verticalAlignment = Alignment.CenterVertically,
+                        horizontalArrangement = Arrangement.SpaceBetween,
+                    ) {
+                        rowButtons.forEach { button ->
+                            ToolButtonItem(
+                                button = button,
+                                state = state,
+                                colors = colors,
+                                backdrop = backdrop,
+                                glassEnabled = buttonGlassEnabled,
+                                labelColor = labelColor,
+                                modifier = Modifier.width(if (buttonGlassEnabled) 48.dp else 40.dp),
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun BottomBarGlassIconButton(
+    onClick: () -> Unit,
+    icon: ImageVector,
+    colors: ReadMenuColors,
+    backdrop: Backdrop?,
+    menuConfig: ReadMenuConfig,
+    glassEnabled: Boolean,
+    contentDescription: String? = null,
+) {
+    ReadMenuGlassIconButton(
+        onClick = onClick,
+        icon = icon,
+        colors = colors,
+        backdrop = backdrop,
+        menuConfig = menuConfig,
+        glassEnabled = glassEnabled,
+        contentDescription = contentDescription,
+    )
+}
+
+@Composable
+internal fun BrightnessBar(
+    brightness: Int,
+    onBrightnessChange: (Int) -> Unit,
+    brightnessAuto: Boolean,
+    onToggleAuto: () -> Unit,
+    onTogglePosition: () -> Unit,
+    modifier: Modifier = Modifier,
+    vertical: Boolean = false,
+    colors: ReadMenuColors,
+    menuConfig: ReadMenuConfig,
+    backdrop: Backdrop? = null,
+    buttonGlassEnabled: Boolean = false,
+    glassThumbEnabled: Boolean = false,
+    onBrightnessPreview: (Int) -> Unit,
+) {
+    var sliderValue by remember(vertical, buttonGlassEnabled) {
+        mutableFloatStateOf(brightness.toFloat())
+    }
+    var sliderDragging by remember(vertical, buttonGlassEnabled) {
+        mutableStateOf(false)
+    }
+
+    LaunchedEffect(brightness, brightnessAuto) {
+        if (brightnessAuto) {
+            sliderDragging = false
+            sliderValue = brightness.toFloat()
+        } else if (!sliderDragging) {
+            sliderValue = brightness.toFloat()
+        }
+    }
+
+    fun updateSliderValue(value: Float) {
+        if (brightnessAuto) return
+        sliderDragging = true
+        sliderValue = value.coerceIn(0f, 100f)
+        val target = value.roundToInt().coerceIn(0, 100)
+
+        //直接先改亮度,如果在这里onBrightnessChange,会ANR
+        onBrightnessPreview(target)
+    }
+
+    fun commitSliderValue(value: Float) {
+        val target = value.roundToInt().coerceIn(0, 100)
+        sliderDragging = false
+        sliderValue = target.toFloat()
+        if (brightnessAuto) return
+        onBrightnessChange(target)
+    }
+
+    fun toggleAuto() {
+        sliderDragging = false
+        sliderValue = brightness.toFloat()
+        onToggleAuto()
+    }
+
+    val brightnessValueDescription = stringResource(
+        R.string.a11y_percent_value,
+        sliderValue.roundToInt().coerceIn(0, 100),
+    )
+
+    if (vertical) {
+        Column(
+            modifier = modifier
+                .width(if (buttonGlassEnabled) 64.dp else 56.dp)
+                .padding(vertical = 12.dp, horizontal = 4.dp),
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.spacedBy(10.dp),
+        ) {
+            ReadMenuGlassIconButton(
+                onClick = ::toggleAuto,
+                icon = Icons.Filled.BrightnessAuto,
+                colors = colors,
+                backdrop = backdrop,
+                menuConfig = menuConfig,
+                glassEnabled = buttonGlassEnabled,
+                selected = brightnessAuto,
+                contentDescription = stringResource(R.string.brightness_follow_system),
+            )
+            AppVerticalSlider(
+                value = sliderValue.coerceIn(0f, 100f),
+                onValueChange = { value ->
+                    if (brightnessAuto) return@AppVerticalSlider
+                    updateSliderValue(value)
+                },
+                onValueChangeFinished = {
+                    commitSliderValue(sliderValue)
+                },
+                valueRange = 0f..100f,
+                enabled = !brightnessAuto,
+                height = 168.dp,
+                accessibilityLabel = stringResource(R.string.brightness),
+                accessibilityValue = brightnessValueDescription,
+            )
+            ReadMenuGlassIconButton(
+                onClick = onTogglePosition,
+                icon = Icons.Filled.SwapHoriz,
+                colors = colors,
+                backdrop = backdrop,
+                menuConfig = menuConfig,
+                glassEnabled = buttonGlassEnabled,
+                contentDescription = stringResource(R.string.brightness_bar_position),
+            )
+        }
+    } else {
+        val buttonSize = if (buttonGlassEnabled) 48.dp else 40.dp
+        Row(
+            modifier = modifier
+                .fillMaxWidth()
+                .padding(horizontal = 16.dp, vertical = 4.dp),
+            verticalAlignment = Alignment.CenterVertically,
+            horizontalArrangement = Arrangement.spacedBy(8.dp),
+        ) {
+            ReadMenuGlassIconButton(
+                onClick = ::toggleAuto,
+                icon = Icons.Filled.BrightnessAuto,
+                colors = colors,
+                backdrop = backdrop,
+                menuConfig = menuConfig,
+                glassEnabled = buttonGlassEnabled,
+                selected = brightnessAuto,
+                contentDescription = stringResource(R.string.brightness_follow_system),
+            )
+            ReadMenuSlider(
+                value = sliderValue.coerceIn(0f, 100f),
+                onValueChange = { value ->
+                    if (brightnessAuto) return@ReadMenuSlider
+                    updateSliderValue(value)
+                },
+                onValueChangeFinished = {
+                    commitSliderValue(sliderValue)
+                },
+                onValueCommit = ::commitSliderValue,
+                valueRange = 0f..100f,
+                enabled = !brightnessAuto,
+                backdrop = backdrop,
+                glassThumbEnabled = glassThumbEnabled,
+                accessibilityLabel = stringResource(R.string.brightness),
+                accessibilityValue = brightnessValueDescription,
+                modifier = Modifier
+                    .weight(1f)
+                    .padding(horizontal = 8.dp),
+            )
+            Spacer(Modifier.width(buttonSize))
+        }
+    }
+}

+ 438 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarFloating.kt

@@ -0,0 +1,438 @@
+package io.legado.app.ui.book.read
+
+import android.content.Context
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Animation
+import androidx.compose.material.icons.filled.AutoStories
+import androidx.compose.material.icons.filled.Bookmark
+import androidx.compose.material.icons.filled.BugReport
+import androidx.compose.material.icons.filled.CleanHands
+import androidx.compose.material.icons.filled.CloudDownload
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Extension
+import androidx.compose.material.icons.filled.FindReplace
+import androidx.compose.material.icons.filled.Image
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Replay
+import androidx.compose.material.icons.filled.SwapHoriz
+import androidx.compose.material.icons.filled.SwapVert
+import androidx.compose.material.icons.filled.Sync
+import androidx.compose.material.icons.filled.Toc
+import androidx.compose.material.icons.filled.Translate
+import androidx.compose.material.icons.filled.Tune
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.kyant.backdrop.Backdrop
+import io.legado.app.R
+import io.legado.app.data.entities.Book
+import io.legado.app.data.repository.ReadPreferences
+import io.legado.app.ui.book.read.sheet.readMenuButtonInfos
+import io.legado.app.ui.theme.LegadoTheme
+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.RoundDropdownMenu
+import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
+import io.legado.app.ui.widget.components.reader.readerMenuLiquidGlassAvailable
+
+@Composable
+internal fun FloatingIconRow(
+    state: ReadBookUiState,
+    preferences: ReadPreferences,
+    eyeProtectionActive: Boolean,
+    colors: ReadMenuColors,
+    alignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+    onIntent: (ReadBookIntent) -> Unit,
+    backdrop: Backdrop?,
+) {
+    val context = LocalContext.current
+    val floatingIcons = remember(
+        state.menuConfig.titleBarButtons,
+        state.isReadAloudRunning,
+        state.isAutoPage,
+        state.translationMode,
+        state.useReplaceRule,
+        eyeProtectionActive,
+    ) {
+        loadFloatingIcons(
+            context = context,
+            state = state,
+            preferences = preferences,
+            eyeProtectionActive = eyeProtectionActive,
+            onIntent = onIntent,
+        )
+    }
+
+    if (floatingIcons.isEmpty()) return
+
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
+            .padding(all = 16.dp),
+        horizontalArrangement = when (alignment) {
+            Alignment.Start -> Arrangement.Start
+            Alignment.End -> Arrangement.End
+            else -> Arrangement.Center
+        },
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        floatingIcons.forEach { iconDef ->
+            val customPath = remember(state.menuConfig.titleBarCustomIcons, iconDef.id) {
+                state.menuConfig.titleBarCustomIcons[iconDef.id]
+            }
+            val isCustom = !customPath.isNullOrBlank()
+            val glassEnabled = !isCustom && state.menuConfig.readMenuFloatingIconLiquidGlass &&
+                    readerMenuLiquidGlassAvailable(backdrop)
+            ReadMenuGlassButtonSurface(
+                onClick = iconDef.onClick,
+                colors = colors,
+                backdrop = backdrop,
+                menuConfig = state.menuConfig,
+                glassEnabled = glassEnabled,
+                iconStyle = 1,
+                selected = iconDef.isActive,
+                modifier = Modifier.padding(horizontal = 4.dp),
+                onLongClick = iconDef.onLongClick,
+                contentDescription = iconDef.label,
+            ) {
+                if (isCustom) {
+                    AsyncImage(
+                        model = customPath,
+                        contentDescription = null,
+                        contentScale = ContentScale.Crop,
+                        modifier = Modifier
+                            .size(36.dp)
+                            .clip(CircleShape),
+                    )
+                } else {
+                    Icon(
+                        imageVector = iconDef.icon,
+                        contentDescription = null,
+                        tint = if (iconDef.isActive) LegadoTheme.colorScheme.primary else colors.content,
+                        modifier = Modifier.size(20.dp),
+                    )
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun OverflowDropdownMenu(
+    state: ReadBookUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    expanded: Boolean,
+    onDismiss: () -> Unit,
+) {
+    val showIcon = state.menuConfig.showMenuIcon
+    val menuIcon: (ImageVector) -> (@Composable () -> Unit)? = { imageVector ->
+        if (showIcon) {
+            { MenuItemIcon(imageVector = imageVector) }
+        } else {
+            null
+        }
+    }
+
+    RoundDropdownMenu(
+        expanded = expanded,
+        onDismissRequest = onDismiss,
+    ) { dismiss ->
+        var imageStyleExpanded by remember { mutableStateOf(false) }
+
+        // Compact mode: moved from top bar buttons
+        if (state.menuConfig.titleBarCompact) {
+            if (!state.isLocalBook) {
+                RoundDropdownMenuItem(
+                    text = stringResource(R.string.change_origin),
+                    leadingIcon = menuIcon(Icons.Default.SwapHoriz),
+                    onClick = { dismiss(); onIntent(ReadBookIntent.MenuChangeSource) },
+                )
+                RoundDropdownMenuItem(
+                    text = stringResource(R.string.menu_refresh_dur),
+                    leadingIcon = menuIcon(Icons.Default.Refresh),
+                    onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshDur) },
+                )
+                RoundDropdownMenuItem(
+                    text = stringResource(R.string.menu_refresh_after),
+                    leadingIcon = menuIcon(Icons.Default.Refresh),
+                    onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshAfter) },
+                )
+                RoundDropdownMenuItem(
+                    text = stringResource(R.string.offline_cache),
+                    leadingIcon = menuIcon(Icons.Default.CloudDownload),
+                    onClick = { dismiss(); onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Download)) },
+                )
+            } else {
+                if (state.isLocalTxt) {
+                    RoundDropdownMenuItem(
+                        text = stringResource(R.string.txt_toc_rule),
+                        leadingIcon = menuIcon(Icons.Default.Toc),
+                        onClick = { dismiss(); onIntent(ReadBookIntent.MenuTocRegex) },
+                    )
+                }
+                RoundDropdownMenuItem(
+                    text = stringResource(R.string.set_charset),
+                    leadingIcon = menuIcon(Icons.Default.Translate),
+                    onClick = { dismiss(); onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Charset)) },
+                )
+            }
+            PillDivider()
+        }
+
+        // 内容处理
+        RoundDropdownMenuItem(
+            text = stringResource(R.string.edit_content),
+            leadingIcon = menuIcon(Icons.Default.Edit),
+            onClick = {
+                dismiss()
+                onIntent(ReadBookIntent.OpenContentEdit)
+            },
+        )
+        RoundDropdownMenuItem(
+            text = stringResource(R.string.bookmark_add),
+            leadingIcon = menuIcon(Icons.Default.Bookmark),
+            onClick = { dismiss(); onIntent(ReadBookIntent.AddBookmark) },
+        )
+        RoundDropdownMenuItem(
+            text = stringResource(R.string.highlight_rule_config),
+            leadingIcon = menuIcon(Icons.Default.Tune),
+            onClick = {
+                dismiss()
+                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.HighlightRuleConfig))
+            },
+        )
+        RoundDropdownMenuItem(
+            text = stringResource(R.string.text_processing),
+            leadingIcon = menuIcon(Icons.Default.FindReplace),
+            onClick = {
+                dismiss()
+                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing))
+            },
+        )
+        RoundDropdownMenuItem(
+            text = stringResource(R.string.reverse_content),
+            leadingIcon = menuIcon(Icons.Default.SwapVert),
+            onClick = { dismiss(); onIntent(ReadBookIntent.MenuReverseContent) },
+        )
+        RoundDropdownMenuItem(
+            text = stringResource(R.string.re_segment),
+            leadingIcon = menuIcon(Icons.Default.Toc),
+            isSelected = state.reSegment,
+            onClick = { onIntent(ReadBookIntent.MenuReSegment) },
+        )
+        if (state.isEpub) {
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.del_ruby_tag),
+                leadingIcon = menuIcon(Icons.Default.CleanHands),
+                isSelected = state.delRubyTag,
+                onClick = { onIntent(ReadBookIntent.MenuDelRubyTag) },
+            )
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.del_h_tag),
+                leadingIcon = menuIcon(Icons.Default.CleanHands),
+                isSelected = state.delHTag,
+                onClick = { onIntent(ReadBookIntent.MenuDelHTag) },
+            )
+        }
+
+        PillDivider()
+
+        // 阅读设置
+        Box {
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.image_style),
+                leadingIcon = menuIcon(Icons.Default.Image),
+                onClick = { imageStyleExpanded = true },
+            )
+            RoundDropdownMenu(
+                expanded = imageStyleExpanded,
+                onDismissRequest = { imageStyleExpanded = false },
+            ) { subDismiss ->
+                listOf(
+                    R.string.btn_default_s to Book.imgStyleDefault,
+                    R.string.image_style_full to Book.imgStyleFull,
+                    R.string.image_style_text to Book.imgStyleText,
+                    R.string.image_style_single to Book.imgStyleSingle,
+                ).forEach { (label, style) ->
+                    RoundDropdownMenuItem(
+                        text = stringResource(label),
+                        onClick = {
+                            subDismiss()
+                            onIntent(ReadBookIntent.MenuImageStyle(style))
+                        },
+                    )
+                }
+            }
+        }
+        RoundDropdownMenuItem(
+            text = stringResource(R.string.book_page_anim),
+            leadingIcon = menuIcon(Icons.Default.Animation),
+            onClick = {
+                dismiss()
+                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.PageAnim))
+            },
+        )
+        RoundDropdownMenuItem(
+            text = stringResource(R.string.simulated_reading),
+            leadingIcon = menuIcon(Icons.Default.AutoStories),
+            onClick = {
+                dismiss()
+                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.SimulatedReading))
+            },
+        )
+
+        PillDivider()
+
+        // 书源
+        if (!state.isLocalBook) {
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.menu_refresh_all),
+                leadingIcon = menuIcon(Icons.Default.Replay),
+                onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshAll) },
+            )
+            PillDivider()
+        }
+
+        // 进度同步
+        if (state.isReadingProgressSyncConfigured) {
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.get_book_progress),
+                leadingIcon = menuIcon(Icons.Default.Sync),
+                onClick = { dismiss(); onIntent(ReadBookIntent.MenuGetProgress) },
+            )
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.cover_book_progress),
+                leadingIcon = menuIcon(Icons.Default.Sync),
+                onClick = { dismiss(); onIntent(ReadBookIntent.MenuCoverProgress) },
+            )
+            PillDivider()
+        }
+
+        // 其他
+        RoundDropdownMenuItem(
+            text = stringResource(R.string.config_btn),
+            leadingIcon = menuIcon(Icons.Default.Extension),
+            onClick = {
+                dismiss()
+                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ToolButtonConfig))
+            },
+        )
+        RoundDropdownMenuItem(
+            text = stringResource(R.string.log),
+            leadingIcon = menuIcon(Icons.Default.BugReport),
+            onClick = {
+                dismiss()
+                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.AppLog))
+            },
+        )
+    }
+}
+
+// ========== Title Bar Icons ==========
+
+private data class FloatingIconDef(
+    val id: String,
+    val icon: ImageVector,
+    val label: String,
+    val isActive: Boolean = false,
+    val onClick: () -> Unit,
+    val onLongClick: (() -> Unit)? = null,
+)
+
+private fun loadFloatingIcons(
+    context: Context,
+    state: ReadBookUiState,
+    preferences: ReadPreferences,
+    eyeProtectionActive: Boolean,
+    onIntent: (ReadBookIntent) -> Unit,
+): List<FloatingIconDef> {
+    val infoMap = readMenuButtonInfos(context).associateBy { it.id }
+
+    val actionMap: Map<String, () -> Unit> = mapOf(
+        "search" to { onIntent(ReadBookIntent.OpenSearch(null)) },
+        "catalog" to { onIntent(ReadBookIntent.OpenChapterList) },
+        "read_aloud" to {
+            if (state.isReadAloudRunning) {
+                onIntent(ReadBookIntent.ReadAloudAction)
+            } else {
+                onIntent(ReadBookIntent.ToggleReadAloud)
+                onIntent(ReadBookIntent.HideMenu)
+            }
+        },
+        "setting" to { onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadStyle)) },
+        "addBookmark" to { onIntent(ReadBookIntent.AddBookmark) },
+        "theme" to { onIntent(ReadBookIntent.ToggleDayNight) },
+        "eye_protection" to { onIntent(ReadBookIntent.ToggleEyeProtection) },
+        "prev_chapter" to { onIntent(ReadBookIntent.PrevChapter) },
+        "next_chapter" to { onIntent(ReadBookIntent.NextChapter) },
+        "replace" to { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing)) },
+        "replace_badge" to { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing)) },
+        "auto_page" to {
+            if (state.isAutoPage) {
+                onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.AutoRead))
+            } else {
+                onIntent(ReadBookIntent.ToggleAutoPage)
+                onIntent(ReadBookIntent.HideMenu)
+            }
+        },
+        "translate" to { onIntent(ReadBookIntent.ToggleTranslation) },
+        "ai_summary" to { onIntent(ReadBookIntent.OpenChapterSummary) },
+        "ai_rewrite" to { onIntent(ReadBookIntent.OpenAiCurrentChapterRewrite) },
+        "more_actions" to { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions)) },
+    )
+
+    val activeIds = buildSet {
+        if (state.isReadAloudRunning) add("read_aloud")
+        if (state.isAutoPage) add("auto_page")
+        if (state.translationMode) add("translate")
+        if (eyeProtectionActive) add("eye_protection")
+    }
+
+    return state.menuConfig.titleBarButtons
+        .asSequence()
+        .filter { it.enabled }
+        .mapNotNull { item ->
+            val id = item.id
+            val info = infoMap[id] ?: return@mapNotNull null
+            FloatingIconDef(
+                id = id,
+                icon = info.icon,
+                label = info.label,
+                isActive = id in activeIds,
+                onClick = actionMap[id] ?: {},
+                onLongClick = when (id) {
+                    "read_aloud" -> {
+                        { onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadAloud)) }
+                    }
+
+                    else -> null
+                },
+            )
+        }
+        .toList()
+}

+ 201 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarSearch.kt

@@ -0,0 +1,201 @@
+package io.legado.app.ui.book.read
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.VerticalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import io.legado.app.R
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.card.TextCard
+import io.legado.app.ui.widget.components.text.AnimatedText
+
+@Composable
+internal fun SearchBottomMenuContent(
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    bottomPadding: Dp = 0.dp,
+) {
+    val totalResults = state.searchResultList.size
+    val currentIndex = state.searchResultIndex.coerceIn(0, (totalResults - 1).coerceAtLeast(0))
+    val percent = if (totalResults > 0) {
+        ((currentIndex + 1) * 100 / totalResults)
+    } else {
+        0
+    }
+
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .windowInsetsPadding(
+                WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)
+            )
+            .padding(top = 12.dp, bottom = bottomPadding)
+            .animateContentSize(),
+    ) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(horizontal = 16.dp),
+            verticalArrangement = Arrangement.spacedBy(8.dp),
+        ) {
+            SearchInfoPill(
+                modifier = Modifier.fillMaxWidth(),
+            ) {
+                AnimatedText(
+                    text = if (totalResults > 0) "${currentIndex + 1} / $totalResults" else "0 / 0",
+                    style = LegadoTheme.typography.labelSmallEmphasized,
+                    color = LegadoTheme.colorScheme.onSurfaceVariant,
+                    maxLines = 1,
+                )
+                Spacer(Modifier.width(8.dp))
+                TextCard(
+                    text = "$percent%"
+                )
+                Spacer(Modifier.width(16.dp))
+                VerticalDivider(
+                    color = LegadoTheme.colorScheme.outlineVariant,
+                    modifier = Modifier
+                        .height(8.dp)
+                        .width(1.dp)
+                )
+                Spacer(Modifier.width(16.dp))
+                AnimatedText(
+                    text = state.chapterName.ifBlank { "-" },
+                    style = LegadoTheme.typography.labelSmallEmphasized,
+                    color = LegadoTheme.colorScheme.onSurfaceVariant,
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis,
+                )
+            }
+        }
+
+        Spacer(Modifier.height(8.dp))
+
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(horizontal = 16.dp),
+            horizontalArrangement = Arrangement.spacedBy(8.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            SearchMenuActionButton(
+                modifier = Modifier.weight(2f),
+                icon = Icons.Default.Search,
+                text = stringResource(R.string.all_results),
+                onClick = {
+                    onIntent(ReadBookIntent.OpenSearch(word = null, autoFocus = false))
+                },
+            )
+            SearchMenuActionButton(
+                modifier = Modifier.weight(1f),
+                icon = Icons.Default.Menu,
+                text = stringResource(R.string.main_menu),
+                onClick = {
+                    onIntent(ReadBookIntent.HideSearchMenu)
+                    onIntent(ReadBookIntent.ShowMenu)
+                },
+            )
+            SearchMenuActionButton(
+                modifier = Modifier.weight(0.55f),
+                icon = Icons.Default.Close,
+                text = null,
+                iconContentDescription = stringResource(R.string.exit),
+                onClick = { onIntent(ReadBookIntent.ExitSearch) },
+            )
+        }
+    }
+}
+
+@Composable
+private fun SearchPillSurface(
+    modifier: Modifier = Modifier,
+    onClick: (() -> Unit)? = null,
+    content: @Composable RowScope.() -> Unit,
+) {
+    val baseModifier = Modifier
+        .height(40.dp)
+        .clip(RoundedCornerShape(16.dp))
+        .background(LegadoTheme.colorScheme.surfaceContainerLow)
+    Row(
+        modifier = modifier
+            .then(
+                if (onClick != null) baseModifier.clickable(
+                    role = Role.Button,
+                    onClick = onClick
+                ) else baseModifier
+            )
+            .padding(horizontal = 12.dp),
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = Arrangement.Center,
+        content = content,
+    )
+}
+
+@Composable
+private fun SearchInfoPill(
+    modifier: Modifier = Modifier,
+    content: @Composable RowScope.() -> Unit,
+) {
+    SearchPillSurface(modifier = modifier, content = content)
+}
+
+@Composable
+private fun SearchMenuActionButton(
+    modifier: Modifier = Modifier,
+    icon: ImageVector,
+    text: String?,
+    iconContentDescription: String? = null,
+    onClick: () -> Unit,
+) {
+    SearchPillSurface(modifier = modifier, onClick = onClick) {
+        Icon(
+            imageVector = icon,
+            contentDescription = iconContentDescription,
+            tint = LegadoTheme.colorScheme.primary,
+            modifier = Modifier.size(16.dp),
+        )
+        if (text != null) {
+            Spacer(Modifier.width(8.dp))
+            Text(
+                text = text,
+                style = LegadoTheme.typography.labelMediumEmphasized,
+                color = LegadoTheme.colorScheme.onSurfaceVariant,
+                maxLines = 1,
+                overflow = TextOverflow.Ellipsis,
+            )
+        }
+    }
+}

+ 117 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarShared.kt

@@ -0,0 +1,117 @@
+package io.legado.app.ui.book.read
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.kyant.backdrop.Backdrop
+import io.legado.app.constant.ReadMenuBlurStyle
+import io.legado.app.help.config.ReadStyleResolver
+import io.legado.app.ui.theme.LegadoTheme
+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.readerMenuLiquidGlass
+import io.legado.app.ui.widget.components.reader.readerMenuSurfaceBrush
+
+internal val menuTextShadow = androidx.compose.ui.graphics.Shadow(
+    color = Color.Black.copy(alpha = 0.12f),
+    offset = Offset.Zero,
+    blurRadius = 12f,
+)
+
+internal data class ReadMenuColors(
+    val background: Color,
+    val content: Color,
+)
+
+internal fun Int.toReaderMenuTintStyle(): ReaderMenuTintStyle {
+    return if (this == ReadMenuBlurStyle.Progressive) {
+        ReaderMenuTintStyle.Gradient
+    } else {
+        ReaderMenuTintStyle.Fill
+    }
+}
+
+@Composable
+internal fun Modifier.readMenuLiquidGlass(
+    backdrop: Backdrop?,
+    colors: ReadMenuColors,
+    shape: Shape,
+    useTopBarStyle: Boolean,
+    useLens: Boolean,
+    blurRadius: Dp? = null,
+    interactive: Boolean = false,
+    surfaceAlphaOverride: Int? = null,
+    menuConfig: ReadMenuConfig,
+): Modifier {
+    val resolvedBlurRadius = blurRadius ?: menuConfig.readMenuBlurRadius.dp
+    val blurAlpha = surfaceAlphaOverride ?: menuConfig.readMenuBlurAlpha
+    val surfaceColor = readMenuTintColor(menuConfig) ?: colors.background
+    val containerColor = surfaceColor.copy(
+        alpha = (blurAlpha.coerceIn(0, 100) / 100f).coerceAtMost(0.6f)
+    )
+    val topBarSurfaceBrush = readerMenuSurfaceBrush(
+        style = ReaderMenuTintStyle.Gradient,
+        placement = ReaderMenuPlacement.Top,
+        color = surfaceColor,
+        alpha = containerColor.alpha,
+    )
+
+    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,
+    )
+}
+
+@Composable
+internal fun readMenuTintColor(menuConfig: ReadMenuConfig): Color? {
+    return menuConfig.readMenuBlurColorNight
+        .takeIf { it != 0 && ReadStyleResolver.isNightTheme() }
+        ?.let(::Color)
+        ?: menuConfig.readMenuBlurColor
+            .takeIf { it != 0 && !ReadStyleResolver.isNightTheme() }
+            ?.let(::Color)
+        ?: menuConfig.readMenuBlurColor
+            .takeIf { it != 0 }
+            ?.let(::Color)
+}
+
+@Composable
+internal fun readMenuBorderColor(menuConfig: ReadMenuConfig): Int {
+    return (if (ReadStyleResolver.isNightTheme()) {
+        menuConfig.readMenuBorderColorNight
+    } else {
+        menuConfig.readMenuBorderColor
+    }).takeIf { it != 0 }
+        ?: LegadoTheme.colorScheme.outlineVariant.hashCode()
+}
+
+@Composable
+internal fun readMenuTextColor(menuConfig: ReadMenuConfig): Color {
+    return Color(
+        if (ReadStyleResolver.isNightTheme()) {
+            menuConfig.readMenuTextColorNight
+        } else {
+            menuConfig.readMenuTextColor
+        }
+    ).takeUnless { it == Color.Unspecified || it.alpha == 0f }
+        ?: LegadoTheme.colorScheme.onSurface
+}

+ 1011 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarTitle.kt

@@ -0,0 +1,1011 @@
+package io.legado.app.ui.book.read
+
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.LocalIndication
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.Login
+import androidx.compose.material.icons.filled.Block
+import androidx.compose.material.icons.filled.CloudDownload
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Extension
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Payment
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.SwapHoriz
+import androidx.compose.material.icons.filled.Toc
+import androidx.compose.material.icons.filled.Translate
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.kyant.backdrop.Backdrop
+import dev.chrisbanes.haze.HazeState
+import io.legado.app.R
+import io.legado.app.constant.ReadMenuBlurMode
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.icon.AppIcons
+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.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.readerMenuLiquidGlassAvailable
+import io.legado.app.ui.widget.components.reader.readerMenuSurfaceBrush
+import io.legado.app.ui.widget.components.settingItem.LocalSliderDragState
+import io.legado.app.ui.widget.components.text.AppText
+import kotlin.math.roundToInt
+
+@Composable
+internal fun MenuTitleBar(
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    backdrop: Backdrop?,
+    hazeState: HazeState?,
+    titleBarMode: String,
+) {
+
+    val topBarBorderWidth = state.menuConfig.readMenuBorderWidth
+    val topBarBorderColor = readMenuBorderColor(state.menuConfig)
+    val isSliderDragging = LocalSliderDragState.current?.isDragging == true
+    val topBarSurfaceAlpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100)
+    val dragTopBarAlpha = topBarSurfaceAlpha.coerceAtMost(30)
+    val animatedTopBarAlpha by animateFloatAsState(
+        targetValue = if (isSliderDragging) dragTopBarAlpha.toFloat() else topBarSurfaceAlpha.toFloat(),
+        animationSpec = tween(durationMillis = 200),
+        label = "ReadMenuTopBarAlpha",
+    )
+    val topBarAlpha = animatedTopBarAlpha / 100f
+    val useTopBarBlur = readMenuTopBarHazeEnabled(hazeState, state.menuConfig)
+    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 = readMenuTextColor(state.menuConfig)
+    val labelStyle = LegadoTheme.typography.labelSmallEmphasized.copy(
+        shadow = menuTextShadow
+    )
+
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .then(
+                if (useTopBarBlur && hazeState != null) {
+                    Modifier.readerMenuHazeEffect(
+                        state = hazeState,
+                        visualState = topBarVisualState,
+                        placement = ReaderMenuPlacement.Top,
+                        baseColor = colors.background,
+                        tintColor = readMenuTintColor(state.menuConfig),
+                        blurRadius = state.menuConfig.readMenuBlurRadius,
+                        surfaceAlpha = animatedTopBarAlpha.roundToInt(),
+                    )
+                } else {
+                    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(
+                if (topBarBorderWidth > 0) {
+                    Modifier.drawBehind {
+                        val strokeWidth = topBarBorderWidth.dp.toPx()
+                        drawLine(
+                            color = Color(topBarBorderColor),
+                            start = Offset(0f, size.height),
+                            end = Offset(size.width, size.height),
+                            strokeWidth = strokeWidth,
+                        )
+                    }
+                } else Modifier
+            )
+            .windowInsetsPadding(
+                WindowInsets.safeDrawing.only(
+                    WindowInsetsSides.Top + WindowInsetsSides.Horizontal
+                )
+            )
+    ) {
+        val useTitleCapsule = state.menuConfig.readMenuTopBarTitleCapsule
+        val capsuleIconColor = LegadoTheme.colorScheme.onSurfaceVariant
+
+        // Title row: left group (back + capsule/title) + right group (actions)
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(horizontal = 16.dp, vertical = 4.dp),
+            verticalAlignment = Alignment.CenterVertically,
+            horizontalArrangement = Arrangement.spacedBy(16.dp),
+        ) {
+            // Left group: back + capsule/title — fills remaining space
+            Row(
+                modifier = Modifier.weight(1f),
+                verticalAlignment = Alignment.CenterVertically,
+            ) {
+                MenuTitleGlassButton(
+                    onClick = { onIntent(ReadBookIntent.CloseReadBook()) },
+                    icon = Icons.AutoMirrored.Filled.ArrowBack,
+                    contentDescription = stringResource(R.string.back),
+                    state = state,
+                    colors = colors,
+                    backdrop = backdrop,
+                )
+
+                if (useTitleCapsule && titleBarMode != "1" && titleBarMode != "3") {
+                    TitleCapsuleGlassLayout(
+                        state = state,
+                        colors = colors,
+                        onIntent = onIntent,
+                        backdrop = backdrop,
+                        titleTextColor = capsuleIconColor,
+                    )
+                } else if (titleBarMode != "1" && titleBarMode != "3") {
+                    AppText(
+                        text = state.bookName,
+                        modifier = Modifier
+                            .weight(1f)
+                            .clickable { onIntent(ReadBookIntent.OpenBookInfo) }
+                            .padding(horizontal = 8.dp, vertical = 4.dp),
+                        style = LegadoTheme.typography.titleMedium.copy(
+                            shadow = menuTextShadow
+                        ),
+                        color = titleTextColor,
+                        maxLines = 2,
+                        overflow = TextOverflow.Ellipsis,
+                    )
+                }
+            }
+
+            // Right group: actions
+            if (readMenuTopBarButtonLiquidGlassEnabled(backdrop, state.menuConfig)) {
+                MenuTitleBarMergedGlassButton(
+                    state = state,
+                    colors = colors,
+                    onIntent = onIntent,
+                    backdrop = backdrop,
+                )
+            } else {
+                val compact = state.menuConfig.titleBarCompact
+                Row(
+                    horizontalArrangement = Arrangement.spacedBy(6.dp),
+                    verticalAlignment = Alignment.CenterVertically,
+                ) {
+                    if (!state.isLocalBook) {
+                        if (!compact && state.bookSource?.customButton == true) {
+                            SourceCustomActionButton(
+                                state = state,
+                                colors = colors,
+                                onIntent = onIntent,
+                                backdrop = backdrop,
+                            )
+                        }
+                        if (!compact) {
+                            SourceActionButton(
+                                state = state,
+                                colors = colors,
+                                onIntent = onIntent,
+                                backdrop = backdrop,
+                            )
+                            RefreshActionButton(
+                                state = state,
+                                colors = colors,
+                                onIntent = onIntent,
+                                backdrop = backdrop,
+                            )
+                            DownloadActionButton(
+                                state = state,
+                                colors = colors,
+                                onIntent = onIntent,
+                                backdrop = backdrop,
+                            )
+                        }
+                    } else if (state.isLocalBook && !compact) {
+                        if (state.isLocalTxt) {
+                            TxtTocRuleActionButton(
+                                state = state,
+                                colors = colors,
+                                onIntent = onIntent,
+                                backdrop = backdrop,
+                            )
+                        }
+                        CharsetActionButton(
+                            state = state,
+                            colors = colors,
+                            onIntent = onIntent,
+                            backdrop = backdrop,
+                        )
+                    }
+
+                    MenuTitleGlassButton(
+                        onClick = {
+                            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions))
+                        },
+                        icon = Icons.Default.MoreVert,
+                        contentDescription = stringResource(R.string.more_actions),
+                        state = state,
+                        colors = colors,
+                        backdrop = backdrop,
+                    )
+                }
+            }
+        }
+
+        // Book name on its own line (mode "1") — hidden when capsule is active
+        if (titleBarMode == "1" && !useTitleCapsule) {
+            AppText(
+                text = state.bookName,
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .clickable { onIntent(ReadBookIntent.OpenBookInfo) }
+                    .padding(horizontal = 16.dp, vertical = 4.dp),
+                style = LegadoTheme.typography.titleMedium.copy(
+                    shadow = menuTextShadow
+                ),
+                color = titleTextColor,
+                maxLines = 2,
+                overflow = TextOverflow.Ellipsis,
+            )
+        }
+
+        // Chapter name + source action (modes "0" and "1") — hidden when capsule is active
+        if ((titleBarMode == "0" || titleBarMode == "1") && !useTitleCapsule) {
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(horizontal = 16.dp, vertical = 4.dp),
+                verticalAlignment = Alignment.CenterVertically,
+            ) {
+                AppText(
+                    text = state.chapterName,
+                    modifier = Modifier
+                        .weight(1f)
+                        .then(
+                            if (!state.isLocalBook) {
+                                Modifier.combinedClickable(
+                                    onClick = { onIntent(ReadBookIntent.OpenChapterUrl) },
+                                    onLongClick = {
+                                        onIntent(ReadBookIntent.ToggleReadUrlInBrowser)
+                                    },
+                                )
+                            } else {
+                                Modifier
+                            }
+                        ),
+                    style = labelStyle,
+                    color = titleTextColor,
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis,
+                )
+
+                if (!state.isLocalBook && state.bookSource != null) {
+                    var sourceMenuExpanded by remember { mutableStateOf(false) }
+                    Box {
+                        AppText(
+                            text = state.bookSource.bookSourceName,
+                            modifier = Modifier
+                                .clickable { sourceMenuExpanded = true }
+                                .padding(start = 8.dp),
+                            style = labelStyle,
+                            color = titleTextColor,
+                            maxLines = 1,
+                        )
+                        RoundDropdownMenu(
+                            expanded = sourceMenuExpanded,
+                            onDismissRequest = { sourceMenuExpanded = false },
+                        ) {
+                            if (!state.bookSource.loginUrl.isNullOrBlank()) {
+                                RoundDropdownMenuItem(
+                                    leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
+                                    text = stringResource(R.string.login),
+                                    onClick = {
+                                        sourceMenuExpanded = false
+                                        onIntent(ReadBookIntent.ShowLogin)
+                                    },
+                                )
+                            }
+                            if (!state.bookSource.getContentRule().payAction.isNullOrBlank()) {
+                                RoundDropdownMenuItem(
+                                    leadingIcon = { MenuItemIcon(Icons.Default.Payment) },
+                                    text = stringResource(R.string.chapter_pay),
+                                    onClick = {
+                                        sourceMenuExpanded = false
+                                        onIntent(ReadBookIntent.PayAction)
+                                    },
+                                )
+                            }
+                            RoundDropdownMenuItem(
+                                leadingIcon = { MenuItemIcon(Icons.Default.Edit) },
+                                text = stringResource(R.string.edit_source),
+                                onClick = {
+                                    sourceMenuExpanded = false
+                                    onIntent(ReadBookIntent.OpenSourceEdit)
+                                },
+                            )
+                            RoundDropdownMenuItem(
+                                leadingIcon = { MenuItemIcon(Icons.Default.Block) },
+                                text = stringResource(R.string.disable_source),
+                                onClick = {
+                                    sourceMenuExpanded = false
+                                    onIntent(ReadBookIntent.DisableSource)
+                                },
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        Spacer(Modifier.height(4.dp))
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun MenuTitleGlassButton(
+    onClick: () -> Unit,
+    icon: ImageVector,
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    backdrop: Backdrop?,
+    modifier: Modifier = Modifier,
+    onLongClick: (() -> Unit)? = null,
+    contentDescription: String? = null,
+) {
+    ReadMenuGlassIconButton(
+        onClick = onClick,
+        icon = icon,
+        colors = colors,
+        backdrop = backdrop,
+        menuConfig = state.menuConfig,
+        glassEnabled = readMenuTopBarButtonLiquidGlassEnabled(backdrop, state.menuConfig),
+        iconStyle = state.menuConfig.titleBarIconStyle,
+        modifier = modifier,
+        onLongClick = onLongClick,
+        contentDescription = contentDescription,
+    )
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun ReadMenuGlassIconButton(
+    onClick: () -> Unit,
+    icon: ImageVector,
+    colors: ReadMenuColors,
+    backdrop: Backdrop?,
+    menuConfig: ReadMenuConfig,
+    glassEnabled: Boolean,
+    iconStyle: Int = menuConfig.readMenuIconStyle,
+    modifier: Modifier = Modifier,
+    onLongClick: (() -> Unit)? = null,
+    selected: Boolean = false,
+    contentDescription: String? = null,
+) {
+    ReadMenuGlassButtonSurface(
+        onClick = onClick,
+        colors = colors,
+        backdrop = backdrop,
+        menuConfig = menuConfig,
+        glassEnabled = glassEnabled,
+        iconStyle = iconStyle,
+        modifier = modifier,
+        onLongClick = onLongClick,
+        selected = selected,
+        contentDescription = contentDescription,
+    ) { tint ->
+        Icon(
+            imageVector = icon,
+            contentDescription = null,
+            tint = tint,
+            modifier = Modifier.size(20.dp),
+        )
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun ReadMenuGlassButtonSurface(
+    onClick: () -> Unit,
+    colors: ReadMenuColors,
+    backdrop: Backdrop?,
+    menuConfig: ReadMenuConfig,
+    glassEnabled: Boolean,
+    iconStyle: Int = menuConfig.readMenuIconStyle,
+    modifier: Modifier = Modifier,
+    onLongClick: (() -> Unit)? = null,
+    selected: Boolean = false,
+    contentDescription: String? = null,
+    content: @Composable (Color) -> Unit,
+) {
+    val shape = CircleShape
+    val tint = when {
+        selected -> LegadoTheme.colorScheme.primary
+        else -> LegadoTheme.colorScheme.onSurfaceVariant
+    }
+    val containerColor = readMenuIconButtonContainerColor(
+        selected = selected,
+        iconStyle = iconStyle,
+    )
+    val border = when {
+        selected -> BorderStroke(1.5.dp, LegadoTheme.colorScheme.secondary)
+        !glassEnabled && iconStyle == 2 -> BorderStroke(1.dp, tint.copy(alpha = 0.45f))
+        else -> null
+    }
+    val outerSize = if (glassEnabled) 48.dp else 40.dp
+    val innerSize = 40.dp
+
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier = modifier
+            .size(outerSize),
+    ) {
+        Box(
+            contentAlignment = Alignment.Center,
+            modifier = Modifier
+                .size(innerSize)
+                .then(
+                    if (glassEnabled) {
+                        Modifier.readMenuLiquidGlass(
+                            backdrop = backdrop,
+                            colors = colors,
+                            shape = shape,
+                            useTopBarStyle = true,
+                            useLens = true,
+                            blurRadius = 32.dp,
+                            interactive = true,
+                            menuConfig = menuConfig,
+                        )
+                    } else {
+                        Modifier
+                            .clip(shape)
+                            .background(containerColor, shape)
+                    }
+                )
+                .then(if (border != null) Modifier.border(border, shape) else Modifier)
+                .combinedClickable(
+                    indication = if (glassEnabled) null else LocalIndication.current,
+                    interactionSource = remember { MutableInteractionSource() },
+                    role = Role.Button,
+                    onLongClick = onLongClick,
+                    onClick = onClick,
+                )
+                .then(
+                    if (contentDescription != null) {
+                        Modifier.semantics { this.contentDescription = contentDescription }
+                    } else {
+                        Modifier
+                    }
+                ),
+        ) {
+            content(tint)
+        }
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun RowScope.TitleCapsuleGlassLayout(
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    backdrop: Backdrop?,
+    titleTextColor: Color,
+) {
+    val pillShape = RoundedCornerShape(50)
+    val glassEnabled = readerMenuLiquidGlassAvailable(backdrop)
+            && state.menuConfig.readMenuTopBarLiquidGlassButtons
+    val iconStyle = state.menuConfig.titleBarIconStyle
+
+    Row(
+        modifier = Modifier
+            .weight(1f)
+            .padding(start = 12.dp)
+            .height(40.dp)
+            .then(
+                if (glassEnabled) {
+                    Modifier.readMenuLiquidGlass(
+                        backdrop = backdrop,
+                        colors = colors,
+                        shape = pillShape,
+                        useTopBarStyle = true,
+                        useLens = false,
+                        blurRadius = 32.dp,
+                        menuConfig = state.menuConfig,
+                    )
+                } else {
+                    val containerColor = when (iconStyle) {
+                        1 -> LegadoTheme.colorScheme.surfaceContainerLow
+                        else -> Color.Transparent
+                    }
+                    val border = when (iconStyle) {
+                        2 -> BorderStroke(
+                            1.dp,
+                            LegadoTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.45f)
+                        )
+
+                        else -> null
+                    }
+                    Modifier
+                        .clip(pillShape)
+                        .background(containerColor, pillShape)
+                        .then(if (border != null) Modifier.border(border, pillShape) else Modifier)
+                }
+            )
+            .then(
+                if (!state.isLocalBook) {
+                    Modifier.combinedClickable(
+                        indication = if (glassEnabled) null else LocalIndication.current,
+                        interactionSource = remember { MutableInteractionSource() },
+                        onClick = { onIntent(ReadBookIntent.OpenBookInfo) },
+                        onLongClick = { onIntent(ReadBookIntent.OpenChapterUrl) },
+                    )
+                } else {
+                    Modifier.clickable(
+                        indication = if (glassEnabled) null else LocalIndication.current,
+                        interactionSource = remember { MutableInteractionSource() },
+                    ) { onIntent(ReadBookIntent.OpenBookInfo) }
+                }
+            )
+            .padding(horizontal = 12.dp),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        Column(
+            modifier = Modifier.weight(1f),
+        ) {
+            AppText(
+                text = state.bookName,
+                style = LegadoTheme.typography.labelMediumEmphasized,
+                color = titleTextColor,
+                maxLines = 1,
+                overflow = TextOverflow.Ellipsis,
+            )
+            if (state.chapterName.isNotBlank()) {
+                AppText(
+                    text = state.chapterName,
+                    style = LegadoTheme.typography.labelSmall,
+                    color = titleTextColor.copy(alpha = 0.7f),
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis,
+                )
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun MergedGlassIconButton(
+    icon: ImageVector,
+    tint: Color,
+    contentDescription: String?,
+    onClick: () -> Unit,
+    onLongClick: (() -> Unit)? = null,
+) {
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier = Modifier
+            .size(40.dp)
+            .then(
+                if (onLongClick != null) {
+                    Modifier.combinedClickable(
+                        indication = null,
+                        interactionSource = remember { MutableInteractionSource() },
+                        role = Role.Button,
+                        onClick = onClick,
+                        onLongClick = onLongClick,
+                    )
+                } else {
+                    Modifier.clickable(
+                        indication = null,
+                        interactionSource = remember { MutableInteractionSource() },
+                        role = Role.Button,
+                        onClick = onClick,
+                    )
+                }
+            )
+            .semantics { contentDescription?.let { this.contentDescription = it } },
+    ) {
+        Icon(
+            imageVector = icon,
+            contentDescription = null,
+            tint = tint,
+            modifier = Modifier.size(20.dp),
+        )
+    }
+}
+
+@Composable
+private fun MergedGlassDivider(tint: Color) {
+    Box(
+        modifier = Modifier
+            .width(1.dp)
+            .height(20.dp)
+            .background(tint.copy(alpha = 0.15f))
+            .clearAndSetSemantics { }
+    )
+}
+
+@Composable
+private fun ChangeSourceMenuItems(
+    dismiss: () -> Unit,
+    onBookChange: () -> Unit,
+    onChapterChange: () -> Unit,
+) {
+    RoundDropdownMenuItem(
+        text = stringResource(R.string.change_origin),
+        onClick = { dismiss(); onBookChange() },
+    )
+    RoundDropdownMenuItem(
+        text = stringResource(R.string.chapter_change_source),
+        onClick = { dismiss(); onChapterChange() },
+    )
+}
+
+@Composable
+private fun RefreshMenuItems(
+    dismiss: () -> Unit,
+    onRefreshDur: () -> Unit,
+    onRefreshAfter: () -> Unit,
+) {
+    RoundDropdownMenuItem(
+        text = stringResource(R.string.menu_refresh_dur),
+        onClick = { dismiss(); onRefreshDur() },
+    )
+    RoundDropdownMenuItem(
+        text = stringResource(R.string.menu_refresh_after),
+        onClick = { dismiss(); onRefreshAfter() },
+    )
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun MenuTitleBarMergedGlassButton(
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    backdrop: Backdrop?,
+) {
+    var sourceExpanded by remember { mutableStateOf(false) }
+    var refreshExpanded by remember { mutableStateOf(false) }
+
+    val pillShape = RoundedCornerShape(50)
+    val tint = LegadoTheme.colorScheme.onSurfaceVariant
+    val compact = state.menuConfig.titleBarCompact
+
+    Box {
+        Row(
+            modifier = Modifier
+                .height(40.dp)
+                .readMenuLiquidGlass(
+                    backdrop = backdrop,
+                    colors = colors,
+                    shape = pillShape,
+                    useTopBarStyle = true,
+                    useLens = true,
+                    blurRadius = 32.dp,
+                    interactive = true,
+                    menuConfig = state.menuConfig,
+                ),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            // SwapHoriz - change source
+            if (!state.isLocalBook && !compact) {
+                if (state.bookSource?.customButton == true) {
+                    MergedGlassIconButton(
+                        icon = Icons.Default.Extension,
+                        tint = tint,
+                        contentDescription = stringResource(R.string.custom_button),
+                        onClick = { onIntent(ReadBookIntent.SourceCustomButton(false)) },
+                        onLongClick = { onIntent(ReadBookIntent.SourceCustomButton(true)) },
+                    )
+                    MergedGlassDivider(tint)
+                }
+
+                MergedGlassIconButton(
+                    icon = Icons.Default.SwapHoriz,
+                    tint = tint,
+                    contentDescription = stringResource(R.string.change_origin),
+                    onClick = { onIntent(ReadBookIntent.MenuChangeSource) },
+                    onLongClick = { sourceExpanded = true },
+                )
+                MergedGlassDivider(tint)
+
+                // Refresh
+                MergedGlassIconButton(
+                    icon = Icons.Default.Refresh,
+                    tint = tint,
+                    contentDescription = stringResource(R.string.menu_refresh_dur),
+                    onClick = { onIntent(ReadBookIntent.MenuRefreshDur) },
+                    onLongClick = { refreshExpanded = true },
+                )
+                MergedGlassDivider(tint)
+
+                // Download
+                MergedGlassIconButton(
+                    icon = Icons.Default.CloudDownload,
+                    tint = tint,
+                    contentDescription = stringResource(R.string.offline_cache),
+                    onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Download)) },
+                )
+                MergedGlassDivider(tint)
+            } else if (state.isLocalBook && !compact) {
+                // TXT directory rule
+                if (state.isLocalTxt) {
+                    MergedGlassIconButton(
+                        icon = Icons.Default.Toc,
+                        tint = tint,
+                        contentDescription = stringResource(R.string.txt_toc_rule),
+                        onClick = { onIntent(ReadBookIntent.MenuTocRegex) },
+                    )
+                    MergedGlassDivider(tint)
+                }
+
+                // Text encoding
+                MergedGlassIconButton(
+                    icon = Icons.Default.Translate,
+                    tint = tint,
+                    contentDescription = stringResource(R.string.set_charset),
+                    onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Charset)) },
+                )
+                MergedGlassDivider(tint)
+            }
+
+            // MoreVert - overflow menu
+            MergedGlassIconButton(
+                icon = AppIcons.MoreVert,
+                tint = tint,
+                contentDescription = stringResource(R.string.more_actions),
+                onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions)) },
+            )
+        }
+
+        // Dropdown menus
+        if (!state.isLocalBook) {
+            RoundDropdownMenu(
+                expanded = sourceExpanded,
+                onDismissRequest = { sourceExpanded = false },
+            ) { dismiss ->
+                ChangeSourceMenuItems(
+                    dismiss = dismiss,
+                    onBookChange = { onIntent(ReadBookIntent.MenuBookChangeSource) },
+                    onChapterChange = { onIntent(ReadBookIntent.MenuChapterChangeSource) },
+                )
+            }
+
+            RoundDropdownMenu(
+                expanded = refreshExpanded,
+                onDismissRequest = { refreshExpanded = false },
+            ) { dismiss ->
+                RefreshMenuItems(
+                    dismiss = dismiss,
+                    onRefreshDur = { onIntent(ReadBookIntent.MenuRefreshDur) },
+                    onRefreshAfter = { onIntent(ReadBookIntent.MenuRefreshAfter) },
+                )
+            }
+        }
+
+    }
+}
+
+@Composable
+private fun SourceCustomActionButton(
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    backdrop: Backdrop?,
+) {
+    MenuTitleGlassButton(
+        onClick = { onIntent(ReadBookIntent.SourceCustomButton(false)) },
+        onLongClick = { onIntent(ReadBookIntent.SourceCustomButton(true)) },
+        icon = Icons.Default.Extension,
+        contentDescription = stringResource(R.string.custom_button),
+        state = state,
+        colors = colors,
+        backdrop = backdrop,
+    )
+}
+
+@Composable
+private fun SourceActionButton(
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    backdrop: Backdrop?,
+) {
+    var expanded by remember { mutableStateOf(false) }
+
+    Box {
+        MenuTitleGlassButton(
+            onClick = { onIntent(ReadBookIntent.MenuChangeSource) },
+            onLongClick = { expanded = true },
+            icon = Icons.Default.SwapHoriz,
+            contentDescription = stringResource(R.string.change_origin),
+            state = state,
+            colors = colors,
+            backdrop = backdrop,
+        )
+
+        RoundDropdownMenu(
+            expanded = expanded,
+            onDismissRequest = { expanded = false },
+        ) { dismiss ->
+            ChangeSourceMenuItems(
+                dismiss = dismiss,
+                onBookChange = { onIntent(ReadBookIntent.MenuBookChangeSource) },
+                onChapterChange = { onIntent(ReadBookIntent.MenuChapterChangeSource) },
+            )
+        }
+    }
+}
+
+@Composable
+private fun RefreshActionButton(
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    backdrop: Backdrop?,
+) {
+    var expanded by remember { mutableStateOf(false) }
+
+    Box {
+        MenuTitleGlassButton(
+            onClick = { onIntent(ReadBookIntent.MenuRefreshDur) },
+            onLongClick = { expanded = true },
+            icon = Icons.Default.Refresh,
+            contentDescription = stringResource(R.string.menu_refresh_dur),
+            state = state,
+            colors = colors,
+            backdrop = backdrop,
+        )
+
+        RoundDropdownMenu(
+            expanded = expanded,
+            onDismissRequest = { expanded = false },
+        ) { dismiss ->
+            RefreshMenuItems(
+                dismiss = dismiss,
+                onRefreshDur = { onIntent(ReadBookIntent.MenuRefreshDur) },
+                onRefreshAfter = { onIntent(ReadBookIntent.MenuRefreshAfter) },
+            )
+        }
+    }
+}
+
+@Composable
+private fun DownloadActionButton(
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    backdrop: Backdrop?,
+) {
+    MenuTitleGlassButton(
+        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Download)) },
+        icon = Icons.Default.CloudDownload,
+        contentDescription = stringResource(R.string.offline_cache),
+        state = state,
+        colors = colors,
+        backdrop = backdrop,
+    )
+}
+
+@Composable
+private fun TxtTocRuleActionButton(
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    backdrop: Backdrop?,
+) {
+    MenuTitleGlassButton(
+        onClick = { onIntent(ReadBookIntent.MenuTocRegex) },
+        icon = Icons.Default.Toc,
+        contentDescription = stringResource(R.string.txt_toc_rule),
+        state = state,
+        colors = colors,
+        backdrop = backdrop,
+    )
+}
+
+@Composable
+private fun CharsetActionButton(
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    backdrop: Backdrop?,
+) {
+    MenuTitleGlassButton(
+        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Charset)) },
+        icon = Icons.Default.Translate,
+        contentDescription = stringResource(R.string.set_charset),
+        state = state,
+        colors = colors,
+        backdrop = backdrop,
+    )
+}
+
+private fun readMenuTopBarButtonLiquidGlassEnabled(
+    backdrop: Backdrop?,
+    menuConfig: ReadMenuConfig,
+): Boolean {
+    return menuConfig.readMenuTopBarLiquidGlassButtons &&
+            readerMenuLiquidGlassAvailable(backdrop)
+}
+
+private fun readMenuTopBarHazeEnabled(
+    hazeState: HazeState?,
+    menuConfig: ReadMenuConfig,
+): Boolean {
+    return hazeState != null && menuConfig.readMenuTopBarBlurMode == ReadMenuBlurMode.Haze
+}
+
+@Composable
+private fun readMenuIconButtonContainerColor(selected: Boolean, iconStyle: Int): Color {
+    return when {
+        selected -> LegadoTheme.colorScheme.secondaryContainer
+        iconStyle == 1 -> LegadoTheme.colorScheme.surfaceContainerLow
+        else -> Color.Transparent
+    }
+}

+ 523 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarTool.kt

@@ -0,0 +1,523 @@
+package io.legado.app.ui.book.read
+
+import android.content.Context
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.progressBarRangeInfo
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.setProgress
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.kyant.backdrop.Backdrop
+import com.kyant.backdrop.backdrops.layerBackdrop
+import com.kyant.backdrop.backdrops.rememberBackdrop
+import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
+import com.kyant.backdrop.backdrops.rememberLayerBackdrop
+import com.kyant.backdrop.drawBackdrop
+import com.kyant.backdrop.effects.blur
+import com.kyant.backdrop.effects.lens
+import com.kyant.backdrop.highlight.Highlight
+import com.kyant.backdrop.shadow.InnerShadow
+import com.kyant.backdrop.shadow.Shadow
+import com.kyant.capsule.ContinuousCapsule
+import io.legado.app.ui.animation.DampedDragAnimation
+import io.legado.app.ui.book.read.sheet.ReadMenuButtonInfo
+import io.legado.app.ui.book.read.sheet.readMenuButtonInfos
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.AppSlider
+import kotlin.math.roundToInt
+
+@Composable
+internal fun ReadMenuSlider(
+    value: Float,
+    onValueChange: (Float) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
+    steps: Int = 0,
+    onValueChangeFinished: (() -> Unit)? = null,
+    onValueCommit: ((Float) -> Unit)? = null,
+    backdrop: Backdrop?,
+    glassThumbEnabled: Boolean,
+    accessibilityLabel: String? = null,
+    accessibilityValue: String? = null,
+) {
+    if (glassThumbEnabled && backdrop != null) {
+        ReadMenuLiquidSlider(
+            value = value,
+            onValueChange = onValueChange,
+            valueRange = valueRange,
+            steps = steps,
+            visibilityThreshold = 0.001f,
+            backdrop = backdrop,
+            modifier = modifier,
+            enabled = enabled,
+            onValueChangeFinished = onValueChangeFinished,
+            onValueCommit = onValueCommit,
+            accessibilityLabel = accessibilityLabel,
+            accessibilityValue = accessibilityValue,
+        )
+        return
+    }
+
+    val commitAction = onValueChangeFinished ?: onValueCommit?.let { commit -> { commit(value) } }
+
+    AppSlider(
+        value = value,
+        onValueChange = onValueChange,
+        modifier = modifier.padding(horizontal = 5.dp),
+        enabled = enabled,
+        valueRange = valueRange,
+        steps = steps,
+        onValueChangeFinished = commitAction,
+        accessibilityLabel = accessibilityLabel,
+        accessibilityValue = accessibilityValue,
+    )
+}
+
+@Composable
+private fun ReadMenuLiquidSlider(
+    value: Float,
+    onValueChange: (Float) -> Unit,
+    valueRange: ClosedFloatingPointRange<Float>,
+    steps: Int = 0,
+    visibilityThreshold: Float,
+    backdrop: Backdrop,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    onValueChangeFinished: (() -> Unit)? = null,
+    onValueCommit: ((Float) -> Unit)? = null,
+    accessibilityLabel: String? = null,
+    accessibilityValue: String? = null,
+) {
+    val accentColor = LegadoTheme.colorScheme.secondary
+    val trackColor = LegadoTheme.colorScheme.surfaceContainerLow
+    val thumbColor =
+        Color.White.copy(alpha = 0.9f).compositeOver(LegadoTheme.colorScheme.surfaceContainerLow)
+    val enabledAlpha = if (enabled) 1f else 0.38f
+
+    val trackBackdrop = rememberLayerBackdrop()
+
+    BoxWithConstraints(
+        modifier
+            .fillMaxWidth()
+            .semantics {
+                accessibilityLabel?.let { contentDescription = it }
+                accessibilityValue?.let { stateDescription = it }
+                progressBarRangeInfo = ProgressBarRangeInfo(
+                    current = value.coerceIn(valueRange),
+                    range = valueRange,
+                    steps = steps,
+                )
+                if (!enabled) {
+                    disabled()
+                }
+                setProgress { target ->
+                    if (!enabled) {
+                        false
+                    } else {
+                        val nextValue = target.coerceIn(valueRange)
+                        onValueChange(nextValue)
+                        onValueCommit?.invoke(nextValue) ?: onValueChangeFinished?.invoke()
+                        true
+                    }
+                }
+            },
+        contentAlignment = Alignment.CenterStart,
+    ) {
+        val trackWidth = constraints.maxWidth
+        val rangeStart = valueRange.start
+        val rangeEnd = valueRange.endInclusive
+        val range = rangeEnd - rangeStart
+        val animationScope = rememberCoroutineScope()
+        val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
+        val dampedDragAnimation =
+            remember(animationScope, trackWidth, rangeStart, rangeEnd, isLtr) {
+                DampedDragAnimation(
+                    animationScope = animationScope,
+                    initialValue = value,
+                    valueRange = valueRange,
+                    visibilityThreshold = visibilityThreshold,
+                    initialScale = 1f,
+                    pressedScale = 1.5f,
+                    onDragStarted = {},
+                    onDragStopped = {
+                        onValueChange(targetValue)
+                        onValueCommit?.invoke(targetValue) ?: onValueChangeFinished?.invoke()
+                    },
+                    onDrag = { _, dragAmount ->
+                        val delta = range * (dragAmount.x / trackWidth)
+                        val nextValue = if (isLtr) {
+                            (targetValue + delta).coerceIn(valueRange)
+                        } else {
+                            (targetValue - delta).coerceIn(valueRange)
+                        }
+                        updateValue(nextValue)
+                        onValueChange(nextValue)
+                    },
+                )
+            }
+
+        LaunchedEffect(dampedDragAnimation, value) {
+            if (dampedDragAnimation.targetValue != value) {
+                dampedDragAnimation.updateValue(value)
+            }
+        }
+
+        val progress = if (range == 0f) {
+            0f
+        } else {
+            ((dampedDragAnimation.value - rangeStart) / range).coerceIn(0f, 1f)
+        }
+
+        Box(Modifier.layerBackdrop(trackBackdrop)) {
+            Box(
+                Modifier
+                    .drawBackdrop(
+                        backdrop = backdrop,
+                        shape = { ContinuousCapsule },
+                        effects = {},
+                        highlight = null,
+                        shadow = {
+                            Shadow(
+                                radius = 8.dp,
+                                color = Color.Black.copy(alpha = 0.12f),
+                            )
+                        },
+                        innerShadow = null,
+                        onDrawSurface = {
+                            drawRect(trackColor.copy(alpha = enabledAlpha))
+                        },
+                    )
+                    .pointerInput(enabled, animationScope, isLtr, trackWidth) {
+                        if (!enabled) return@pointerInput
+                        detectTapGestures { position ->
+                            val delta = range * (position.x / trackWidth)
+                            val targetValue =
+                                (if (isLtr) rangeStart + delta else rangeEnd - delta)
+                                    .coerceIn(valueRange)
+                            dampedDragAnimation.animateToValue(targetValue)
+                            onValueChange(targetValue)
+                            onValueCommit?.invoke(targetValue) ?: onValueChangeFinished?.invoke()
+                        }
+                    }
+                    .height(6f.dp)
+                    .fillMaxWidth(),
+            )
+            Box(
+                Modifier
+                    .clip(ContinuousCapsule)
+                    .background(accentColor.copy(alpha = enabledAlpha))
+                    .height(6f.dp)
+                    .layout { measurable, constraints ->
+                        val placeable = measurable.measure(constraints)
+                        val width = (constraints.maxWidth * progress).roundToInt()
+                        layout(width, placeable.height) {
+                            placeable.place(0, 0)
+                        }
+                    },
+            )
+        }
+
+        Box(
+            Modifier
+                .graphicsLayer {
+                    alpha = enabledAlpha
+                    translationX =
+                        (-size.width / 2f + trackWidth * progress)
+                            .coerceIn(-size.width / 4f, trackWidth - size.width * 3f / 4f) *
+                                if (isLtr) 1f else -1f
+                }
+                .then(if (enabled) dampedDragAnimation.modifier else Modifier)
+                .drawBackdrop(
+                    backdrop = rememberCombinedBackdrop(
+                        backdrop,
+                        rememberBackdrop(trackBackdrop) { drawBackdrop ->
+                            val pressProgress = dampedDragAnimation.pressProgress
+                            val scaleX = 2f / 3f + (1f / 3f) * pressProgress
+                            scale(scaleX, pressProgress) {
+                                drawBackdrop()
+                            }
+                        },
+                    ),
+                    shape = { ContinuousCapsule },
+                    effects = {
+                        val pressProgress = dampedDragAnimation.pressProgress
+                        blur(8.dp.toPx() * (1f - pressProgress))
+                        lens(
+                            10.dp.toPx() * pressProgress,
+                            14.dp.toPx() * pressProgress,
+                            chromaticAberration = true,
+                        )
+                    },
+                    highlight = {
+                        Highlight.Ambient.copy(
+                            width = Highlight.Ambient.width / 1.5f,
+                            blurRadius = Highlight.Ambient.blurRadius / 1.5f,
+                            alpha = dampedDragAnimation.pressProgress,
+                        )
+                    },
+                    shadow = {
+                        Shadow(
+                            radius = 8.dp,
+                            color = Color.Black.copy(alpha = 0.12f),
+                        )
+                    },
+                    innerShadow = {
+                        InnerShadow(
+                            radius = 4.dp * dampedDragAnimation.pressProgress,
+                            alpha = dampedDragAnimation.pressProgress,
+                        )
+                    },
+                    layerBlock = {
+                        scaleX = dampedDragAnimation.scaleX
+                        scaleY = dampedDragAnimation.scaleY
+                        val velocity = dampedDragAnimation.velocity / 10f
+                        scaleX /= 1f - (velocity * 0.75f).coerceIn(-0.2f, 0.2f)
+                        scaleY *= 1f - (velocity * 0.25f).coerceIn(-0.2f, 0.2f)
+                    },
+                    onDrawSurface = {
+                        val pressProgress = dampedDragAnimation.pressProgress
+                        drawRect(thumbColor.copy(alpha = 1f - pressProgress))
+                    },
+                )
+                .size(40f.dp, 24f.dp),
+        )
+    }
+}
+
+@Composable
+internal fun ToolButtonItem(
+    button: ToolButtonDef,
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    backdrop: Backdrop?,
+    glassEnabled: Boolean,
+    labelColor: Color,
+    modifier: Modifier = Modifier,
+) {
+    val badgeCount = when (button.id) {
+        "replace_badge" -> state.effectiveReplaceCount
+        else -> 0
+    }
+
+    Column(
+        modifier = modifier,
+        horizontalAlignment = Alignment.CenterHorizontally
+    ) {
+        ReadMenuGlassButtonSurface(
+            onClick = button.onClick,
+            colors = colors,
+            backdrop = backdrop,
+            menuConfig = state.menuConfig,
+            glassEnabled = glassEnabled,
+            selected = button.isActive,
+            onLongClick = button.onLongClick,
+            contentDescription = button.description,
+        ) { tint ->
+            ToolButtonContent(
+                button = button,
+                tint = tint,
+                badgeCount = badgeCount,
+            )
+        }
+        if (state.menuConfig.readMenuIconShowText) {
+            Spacer(Modifier.height(2.dp))
+            Text(
+                text = button.description,
+                style = LegadoTheme.typography.labelSmall.copy(
+                    shadow = menuTextShadow
+                ),
+                color = labelColor,
+                maxLines = 1,
+                modifier = Modifier.wrapContentWidth(
+                    align = Alignment.CenterHorizontally,
+                    unbounded = true,
+                ),
+            )
+        }
+    }
+}
+
+@Composable
+private fun ToolButtonContent(
+    button: ToolButtonDef,
+    tint: Color,
+    badgeCount: Int,
+) {
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier = Modifier.fillMaxSize(),
+    ) {
+        if (button.customIconPath.isNullOrBlank()) {
+            Icon(
+                imageVector = button.icon,
+                contentDescription = null,
+                modifier = Modifier.size(20.dp),
+                tint = tint,
+            )
+        } else {
+            AsyncImage(
+                model = button.customIconPath,
+                contentDescription = null,
+                contentScale = ContentScale.Crop,
+                modifier = Modifier
+                    .size(36.dp)
+                    .clip(CircleShape),
+            )
+        }
+        if (badgeCount > 0) {
+            Text(
+                text = badgeCount.toString(),
+                modifier = Modifier
+                    .align(Alignment.TopEnd)
+                    .background(
+                        LegadoTheme.colorScheme.error,
+                        RoundedCornerShape(8.dp),
+                    )
+                    .padding(horizontal = 4.dp, vertical = 1.dp),
+                style = LegadoTheme.typography.labelSmall,
+                color = LegadoTheme.colorScheme.onError,
+            )
+        }
+    }
+}
+
+internal data class ToolButtonDef(
+    val id: String,
+    val icon: ImageVector,
+    val description: String,
+    val customIconPath: String?,
+    val isActive: Boolean = false,
+    val onClick: () -> Unit,
+    val onLongClick: (() -> Unit)? = null,
+)
+
+internal fun loadToolButtons(
+    context: Context,
+    state: ReadBookUiState,
+    eyeProtectionEnabled: Boolean,
+    onIntent: (ReadBookIntent) -> Unit,
+): List<ToolButtonDef> {
+    val customIcons = state.menuConfig.readMenuCustomIcons
+    fun ReadMenuButtonInfo.toButton(
+        isActive: Boolean = false,
+        onLongClick: (() -> Unit)? = null,
+        onClick: () -> Unit,
+    ): ToolButtonDef {
+        return ToolButtonDef(id, icon, label, customIcons[id], isActive, onClick, onLongClick)
+    }
+
+    val infoMap = readMenuButtonInfos(context).associateBy { it.id }
+    val allButtons = listOf(
+        infoMap.getValue("search").toButton {
+            onIntent(ReadBookIntent.OpenSearch(null))
+        },
+        infoMap.getValue("catalog").toButton {
+            onIntent(ReadBookIntent.OpenChapterList)
+        },
+        infoMap.getValue("read_aloud").toButton(
+            isActive = state.isReadAloudRunning,
+            onLongClick = {
+                onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadAloud))
+            },
+        ) {
+            if (state.isReadAloudRunning) {
+                onIntent(ReadBookIntent.ReadAloudAction)
+            } else {
+                onIntent(ReadBookIntent.ToggleReadAloud)
+                onIntent(ReadBookIntent.HideMenu)
+            }
+        },
+        infoMap.getValue("setting").toButton {
+            onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadStyle))
+        },
+        infoMap.getValue("addBookmark").toButton {
+            onIntent(ReadBookIntent.AddBookmark)
+        },
+        infoMap.getValue("theme").toButton {
+            onIntent(ReadBookIntent.ToggleDayNight)
+        },
+        infoMap.getValue("eye_protection").toButton(
+            isActive = eyeProtectionEnabled,
+            onLongClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.EyeProtection)) },
+        ) {
+            onIntent(ReadBookIntent.ToggleEyeProtection)
+        },
+        infoMap.getValue("prev_chapter").toButton {
+            onIntent(ReadBookIntent.PrevChapter)
+        },
+        infoMap.getValue("next_chapter").toButton {
+            onIntent(ReadBookIntent.NextChapter)
+        },
+        infoMap.getValue("replace").toButton(
+        ) {
+            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing))
+        },
+        infoMap.getValue("replace_badge").toButton {
+            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing))
+        },
+        infoMap.getValue("auto_page").toButton(isActive = state.isAutoPage) {
+            if (state.isAutoPage) {
+                onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.AutoRead))
+            } else {
+                onIntent(ReadBookIntent.ToggleAutoPage)
+                onIntent(ReadBookIntent.HideMenu)
+            }
+        },
+        infoMap.getValue("translate").toButton(isActive = state.translationMode) {
+            onIntent(ReadBookIntent.ToggleTranslation)
+        },
+        infoMap.getValue("ai_summary").toButton {
+            onIntent(ReadBookIntent.OpenChapterSummary)
+        },
+        infoMap.getValue("ai_rewrite").toButton {
+            onIntent(ReadBookIntent.OpenAiCurrentChapterRewrite)
+        },
+        infoMap.getValue("more_actions").toButton {
+            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions))
+        },
+    )
+
+    val allMap = allButtons.associateBy { it.id }
+    return state.menuConfig.bottomBarButtons
+        .asSequence()
+        .filter { it.enabled }
+        .mapNotNull { allMap[it.id] }
+        .toList()
+}