Procházet zdrojové kódy

feat: 优化了m3的按钮

HapeLee před 3 měsíci
rodič
revize
46c91fbe8c

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

@@ -220,6 +220,8 @@ object PreferKey {
     const val bottomBarBlurAlpha = "bottomBarBlurAlpha"
     const val bottomBarBlurAlpha = "bottomBarBlurAlpha"
     const val bottomBarLensRadius = "bottomBarLensRadius"
     const val bottomBarLensRadius = "bottomBarLensRadius"
     const val useFlexibleTopAppBar = "useFlexibleTopAppBar"
     const val useFlexibleTopAppBar = "useFlexibleTopAppBar"
+    const val topBarButtonStyle = "topBarButtonStyle"
+    const val mergeTopBarActions = "mergeTopBarActions"
     const val bookInfoFollowCoverColor = "bookInfoFollowCoverColor"
     const val bookInfoFollowCoverColor = "bookInfoFollowCoverColor"
     const val bookInfoBackgroundBlur = "bookInfoBackgroundBlur"
     const val bookInfoBackgroundBlur = "bookInfoBackgroundBlur"
     const val bookInfoNetworkCoverBackground = "bookInfoNetworkCoverBackground"
     const val bookInfoNetworkCoverBackground = "bookInfoNetworkCoverBackground"

+ 4 - 0
app/src/main/java/io/legado/app/data/repository/FeatureSettingsRepositories.kt

@@ -385,6 +385,8 @@ internal fun Preferences.toThemeSettings(): ThemeSettings = ThemeSettings(
     bottomBarBlurAlpha = compatDsInt(PreferKey.bottomBarBlurAlpha) ?: 40,
     bottomBarBlurAlpha = compatDsInt(PreferKey.bottomBarBlurAlpha) ?: 40,
     bottomBarLensRadius = compatDsFloat(PreferKey.bottomBarLensRadius) ?: 24f,
     bottomBarLensRadius = compatDsFloat(PreferKey.bottomBarLensRadius) ?: 24f,
     useFlexibleTopAppBar = compatDsBoolean(PreferKey.useFlexibleTopAppBar) ?: true,
     useFlexibleTopAppBar = compatDsBoolean(PreferKey.useFlexibleTopAppBar) ?: true,
+    topBarButtonStyle = compatDsString(PreferKey.topBarButtonStyle) ?: "tonal",
+    mergeTopBarActions = compatDsBoolean(PreferKey.mergeTopBarActions) ?: false,
     bookInfoFollowCoverColor = compatDsBoolean(PreferKey.bookInfoFollowCoverColor) ?: true,
     bookInfoFollowCoverColor = compatDsBoolean(PreferKey.bookInfoFollowCoverColor) ?: true,
     bookInfoNetworkCoverBackground =
     bookInfoNetworkCoverBackground =
         compatDsString(PreferKey.bookInfoNetworkCoverBackground) ?: "on",
         compatDsString(PreferKey.bookInfoNetworkCoverBackground) ?: "on",
@@ -464,6 +466,8 @@ internal fun ThemeSettings.toGatewayPrefMap(): Map<String, Any?> = mapOf(
     PreferKey.bottomBarBlurAlpha to bottomBarBlurAlpha,
     PreferKey.bottomBarBlurAlpha to bottomBarBlurAlpha,
     PreferKey.bottomBarLensRadius to bottomBarLensRadius,
     PreferKey.bottomBarLensRadius to bottomBarLensRadius,
     PreferKey.useFlexibleTopAppBar to useFlexibleTopAppBar,
     PreferKey.useFlexibleTopAppBar to useFlexibleTopAppBar,
+    PreferKey.topBarButtonStyle to topBarButtonStyle,
+    PreferKey.mergeTopBarActions to mergeTopBarActions,
     PreferKey.bookInfoFollowCoverColor to bookInfoFollowCoverColor,
     PreferKey.bookInfoFollowCoverColor to bookInfoFollowCoverColor,
     PreferKey.bookInfoNetworkCoverBackground to bookInfoNetworkCoverBackground,
     PreferKey.bookInfoNetworkCoverBackground to bookInfoNetworkCoverBackground,
     PreferKey.bookInfoDefaultCoverBackground to bookInfoDefaultCoverBackground,
     PreferKey.bookInfoDefaultCoverBackground to bookInfoDefaultCoverBackground,

+ 4 - 0
app/src/main/java/io/legado/app/data/repository/ThemePackageSettingsRepository.kt

@@ -96,6 +96,8 @@ class ThemePackageSettingsRepository : ThemePackageSettingsGateway {
         navIconMy = string(PreferKey.navIconMy, ""),
         navIconMy = string(PreferKey.navIconMy, ""),
         useMiuixMonet = boolean(PreferKey.useMiuixMonet, false),
         useMiuixMonet = boolean(PreferKey.useMiuixMonet, false),
         useFlexibleTopAppBar = boolean(PreferKey.useFlexibleTopAppBar, true),
         useFlexibleTopAppBar = boolean(PreferKey.useFlexibleTopAppBar, true),
+        topBarButtonStyle = string(PreferKey.topBarButtonStyle, "tonal"),
+        mergeTopBarActions = boolean(PreferKey.mergeTopBarActions, false),
         bgImageLight = AppConfigStore.getString(PreferKey.bgImage),
         bgImageLight = AppConfigStore.getString(PreferKey.bgImage),
         bgImageDark = AppConfigStore.getString(PreferKey.bgImageN),
         bgImageDark = AppConfigStore.getString(PreferKey.bgImageN),
         bgImageBlurring = int(PreferKey.bgImageBlurring, 0),
         bgImageBlurring = int(PreferKey.bgImageBlurring, 0),
@@ -225,6 +227,8 @@ internal fun ThemeExportData.toPreferenceValues(): Map<String, Any?> {
                 PreferKey.navIconMy to data.navIconMy,
                 PreferKey.navIconMy to data.navIconMy,
                 PreferKey.useMiuixMonet to data.useMiuixMonet,
                 PreferKey.useMiuixMonet to data.useMiuixMonet,
                 PreferKey.useFlexibleTopAppBar to data.useFlexibleTopAppBar,
                 PreferKey.useFlexibleTopAppBar to data.useFlexibleTopAppBar,
+        PreferKey.topBarButtonStyle to data.topBarButtonStyle,
+        PreferKey.mergeTopBarActions to data.mergeTopBarActions,
                 PreferKey.bgImage to data.bgImageLight,
                 PreferKey.bgImage to data.bgImageLight,
                 PreferKey.bgImageN to data.bgImageDark,
                 PreferKey.bgImageN to data.bgImageDark,
                 PreferKey.bgImageBlurring to data.bgImageBlurring,
                 PreferKey.bgImageBlurring to data.bgImageBlurring,

+ 2 - 0
app/src/main/java/io/legado/app/domain/model/settings/ThemeExportData.kt

@@ -79,6 +79,8 @@ data class ThemeExportData(
     val navIconMy: String = "",
     val navIconMy: String = "",
     val useMiuixMonet: Boolean = false,
     val useMiuixMonet: Boolean = false,
     val useFlexibleTopAppBar: Boolean = true,
     val useFlexibleTopAppBar: Boolean = true,
+    val topBarButtonStyle: String = "tonal",
+    val mergeTopBarActions: Boolean = false,
     val bgImageLight: String? = null,
     val bgImageLight: String? = null,
     val bgImageDark: String? = null,
     val bgImageDark: String? = null,
     val bgImageBlurring: Int = 0,
     val bgImageBlurring: Int = 0,

+ 2 - 0
app/src/main/java/io/legado/app/domain/model/settings/ThemeSettings.kt

@@ -42,6 +42,8 @@ data class ThemeSettings(
     val bottomBarBlurAlpha: Int = 40,
     val bottomBarBlurAlpha: Int = 40,
     val bottomBarLensRadius: Float = 24f,
     val bottomBarLensRadius: Float = 24f,
     val useFlexibleTopAppBar: Boolean = true,
     val useFlexibleTopAppBar: Boolean = true,
+    val topBarButtonStyle: String = "tonal",
+    val mergeTopBarActions: Boolean = false,
     val bookInfoFollowCoverColor: Boolean = true,
     val bookInfoFollowCoverColor: Boolean = true,
     val bookInfoNetworkCoverBackground: String = "on",
     val bookInfoNetworkCoverBackground: String = "on",
     val bookInfoDefaultCoverBackground: String = "on",
     val bookInfoDefaultCoverBackground: String = "on",

+ 2 - 0
app/src/main/java/io/legado/app/help/storage/BackupConfig.kt

@@ -276,6 +276,8 @@ object BackupConfig {
         PreferKey.bottomBarBlurAlpha,
         PreferKey.bottomBarBlurAlpha,
         PreferKey.bottomBarLensRadius,
         PreferKey.bottomBarLensRadius,
         PreferKey.useFlexibleTopAppBar,
         PreferKey.useFlexibleTopAppBar,
+        PreferKey.topBarButtonStyle,
+        PreferKey.mergeTopBarActions,
         PreferKey.bookInfoFollowCoverColor,
         PreferKey.bookInfoFollowCoverColor,
         PreferKey.bookInfoBackgroundBlur,
         PreferKey.bookInfoBackgroundBlur,
         PreferKey.bookInfoNetworkCoverBackground,
         PreferKey.bookInfoNetworkCoverBackground,

+ 18 - 0
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt

@@ -532,6 +532,24 @@ fun ThemeConfigScreen(
                                 updateTheme { it.copy(useFlexibleTopAppBar = value) }
                                 updateTheme { it.copy(useFlexibleTopAppBar = value) }
                             }
                             }
                         )
                         )
+                        DropdownListSettingItem(
+                            title = stringResource(R.string.top_bar_button_style),
+                            selectedValue = theme.topBarButtonStyle,
+                            displayEntries = stringArrayResource(R.array.top_bar_button_style),
+                            entryValues = stringArrayResource(R.array.top_bar_button_style_value),
+                            onValueChange = { value ->
+                                updateTheme { it.copy(topBarButtonStyle = value) }
+                            }
+                        )
+                        AnimatedVisibility(visible = theme.topBarButtonStyle != "plain") {
+                            SwitchSettingItem(
+                                title = stringResource(R.string.merge_top_bar_actions),
+                                checked = theme.mergeTopBarActions,
+                                onCheckedChange = { value ->
+                                    updateTheme { it.copy(mergeTopBarActions = value) }
+                                }
+                            )
+                        }
                     }
                     }
                     SwitchSettingItem(
                     SwitchSettingItem(
                         title = stringResource(R.string.is_blur_enable),
                         title = stringResource(R.string.is_blur_enable),

+ 2 - 0
app/src/main/java/io/legado/app/ui/widget/components/button/series/SeriesIconButton.kt

@@ -49,6 +49,8 @@ internal val SeriesIconSize: Dp
     get() = IconButtonDefaults.mediumIconSize
     get() = IconButtonDefaults.mediumIconSize
 internal val MediumSeriesIconButtonSize = DpSize(40.dp, 40.dp)
 internal val MediumSeriesIconButtonSize = DpSize(40.dp, 40.dp)
 internal val MediumSeriesIconSize = SeriesIconSize
 internal val MediumSeriesIconSize = SeriesIconSize
+internal val TopBarSeriesIconButtonSize = DpSize(36.dp, 36.dp)
+internal val TopBarSeriesIconSize = 20.dp
 internal val SmallButtonShape = RoundedCornerShape(50)
 internal val SmallButtonShape = RoundedCornerShape(50)
 
 
 internal enum class SeriesIconButtonStyle {
 internal enum class SeriesIconButtonStyle {

+ 2 - 10
app/src/main/java/io/legado/app/ui/widget/components/topbar/GlassMediumFlexibleTopAppBar.kt

@@ -1,11 +1,9 @@
 package io.legado.app.ui.widget.components.topbar
 package io.legado.app.ui.widget.components.topbar
 
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.ExperimentalLayoutApi
 import androidx.compose.foundation.layout.ExperimentalLayoutApi
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.WindowInsets
@@ -135,9 +133,7 @@ fun GlassMediumFlexibleTopAppBar(
                         navigationIcon = navigationIcon,
                         navigationIcon = navigationIcon,
                         actions = {
                         actions = {
                             Box(modifier = Modifier.padding(end = 12.dp)) {
                             Box(modifier = Modifier.padding(end = 12.dp)) {
-                                Row(
-                                    horizontalArrangement = Arrangement.spacedBy(8.dp)
-                                ) { actions() }
+                                TopBarActionsRow { actions() }
                             }
                             }
                         },
                         },
                         scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,
                         scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,
@@ -168,11 +164,7 @@ fun GlassMediumFlexibleTopAppBar(
                         navigationIcon = navigationIcon,
                         navigationIcon = navigationIcon,
                         actions = {
                         actions = {
                             Box(modifier = Modifier.padding(end = 12.dp)) {
                             Box(modifier = Modifier.padding(end = 12.dp)) {
-                                Row(
-                                    horizontalArrangement = Arrangement.spacedBy(8.dp)
-                                ) {
-                                    actions()
-                                }
+                                TopBarActionsRow { actions() }
                             }
                             }
                         },
                         },
                         scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,
                         scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/topbar/GlassTopAppBar.kt

@@ -72,7 +72,7 @@ fun GlassTopAppBar(
                     )
                     )
                 },
                 },
                 navigationIcon = navigationIcon,
                 navigationIcon = navigationIcon,
-                actions = actions,
+                actions = { TopBarActionsRow { actions() } },
                 colors = TopAppBarDefaults.topAppBarColors(
                 colors = TopAppBarDefaults.topAppBarColors(
                     containerColor = Color.Transparent,
                     containerColor = Color.Transparent,
                     scrolledContainerColor = Color.Transparent
                     scrolledContainerColor = Color.Transparent

+ 253 - 50
app/src/main/java/io/legado/app/ui/widget/components/topbar/TopBarButton.kt

@@ -3,60 +3,236 @@ package io.legado.app.ui.widget.components.topbar
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.FilledTonalIconButton
-import androidx.compose.material3.IconButtonDefaults
 import androidx.compose.material3.Text
 import androidx.compose.material3.Text
-import androidx.compose.material3.ToggleButton
+import androidx.compose.material3.ripple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 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.Color
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.R
-import io.legado.app.ui.theme.LocalAppUiConfiguration
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LocalAppUiConfiguration
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.button.series.AnimatedActionButtonCore
 import io.legado.app.ui.widget.components.button.series.AnimatedActionButtonCore
 import io.legado.app.ui.widget.components.button.series.AnimatedIcon
 import io.legado.app.ui.widget.components.button.series.AnimatedIcon
-import io.legado.app.ui.widget.components.button.series.MediumPlainButton
+import io.legado.app.ui.widget.components.button.series.MediumSeriesIconButtonSize
+import io.legado.app.ui.widget.components.button.series.MediumSeriesIconSize
+import io.legado.app.ui.widget.components.button.series.SeriesButton
+import io.legado.app.ui.widget.components.button.series.SeriesIconButtonStyle
+import io.legado.app.ui.widget.components.button.series.TopBarSeriesIconButtonSize
+import io.legado.app.ui.widget.components.button.series.TopBarSeriesIconSize
+import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.icon.AppIcons
 import top.yukonga.miuix.kmp.theme.MiuixTheme
 import top.yukonga.miuix.kmp.theme.MiuixTheme
 import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
 import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
 import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
 import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
 import top.yukonga.miuix.kmp.basic.Text as MiuixText
 import top.yukonga.miuix.kmp.basic.Text as MiuixText
 
 
+/** 顶栏按钮样式配置,与 M3 / 渐进模糊解耦。Glass 预留液态玻璃外观。 */
+enum class TopBarButtonStyle(val storageValue: String) {
+    Plain("plain"),
+    Tonal("tonal"),
+    Outlined("outlined"),
+    Glass("glass");
+
+    companion object {
+        fun fromStorage(value: String?): TopBarButtonStyle =
+            entries.firstOrNull { it.storageValue == value } ?: Tonal
+    }
+}
+
+/** 合并模式下的共享计数器,记录下一个按钮的索引。 */
+internal class TopBarMergeCounter {
+    var index = 0
+}
+
+/** 合并模式的共享状态;null 表示未处于合并模式。 */
+internal val LocalTopBarMergeState = staticCompositionLocalOf<TopBarMergeCounter?> { null }
+
+@Composable
+private fun currentTopBarButtonStyle(): TopBarButtonStyle =
+    TopBarButtonStyle.fromStorage(LocalAppUiConfiguration.current.theme.topBarButtonStyle)
+
+private val TopBarButtonStyle.seriesStyle: SeriesIconButtonStyle
+    get() = when (this) {
+        TopBarButtonStyle.Plain -> SeriesIconButtonStyle.Plain
+        TopBarButtonStyle.Tonal, TopBarButtonStyle.Glass -> SeriesIconButtonStyle.Tonal
+        TopBarButtonStyle.Outlined -> SeriesIconButtonStyle.Outlined
+    }
+
+/** Plain 无容器背景,图标可以更大(40dp/24dp);带容器/边框的样式用紧凑的 36dp/20dp。 */
+private val TopBarButtonStyle.buttonSize: DpSize
+    get() = when (this) {
+        TopBarButtonStyle.Plain -> MediumSeriesIconButtonSize
+        TopBarButtonStyle.Tonal, TopBarButtonStyle.Outlined, TopBarButtonStyle.Glass ->
+            TopBarSeriesIconButtonSize
+    }
+
+private val TopBarButtonStyle.iconSize: Dp
+    get() = when (this) {
+        TopBarButtonStyle.Plain -> MediumSeriesIconSize
+        TopBarButtonStyle.Tonal, TopBarButtonStyle.Outlined, TopBarButtonStyle.Glass ->
+            TopBarSeriesIconSize
+    }
+
+/**
+ * 顶栏 actions 槽按钮间距:Plain 无边框/容器,比带容器/边框的样式更紧凑,
+ * 否则同样的间距会让裸图标显得更分散。
+ */
+@Composable
+internal fun topBarActionSpacing(): Dp {
+    val style = currentTopBarButtonStyle()
+    return if (style == TopBarButtonStyle.Plain) 4.dp else 8.dp
+}
+
+/** 合并模式下按钮左侧的竖向分隔线(首个按钮不画)。 */
+@Composable
+private fun Modifier.mergedDivider(): Modifier {
+    val dividerColor = LegadoTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f)
+    return drawBehind {
+        drawLine(
+            color = dividerColor,
+            start = Offset(0f, size.height * 0.3f),
+            end = Offset(0f, size.height * 0.7f),
+            strokeWidth = 1.dp.toPx()
+        )
+    }
+}
+
+/**
+ * 顶栏 actions 的统一 Row。
+ *
+ * 开启「合并顶栏按钮」且样式为 Tonal/Outlined/Glass 时,把多个按钮的容器/边框
+ * 融合成一个胶囊,按钮间用竖向分隔线隔开(复用 [TopBarMergeCounter] 自动分配索引)。
+ * 单个按钮时胶囊自然退化为普通按钮。Plain 无容器,始终走普通间距 Row。
+ */
+@Composable
+internal fun TopBarActionsRow(
+    modifier: Modifier = Modifier,
+    content: @Composable RowScope.() -> Unit
+) {
+    val style = currentTopBarButtonStyle()
+    val mergeEnabled = LocalAppUiConfiguration.current.theme.mergeTopBarActions
+    if (!mergeEnabled || style == TopBarButtonStyle.Plain) {
+        Row(
+            modifier = modifier,
+            horizontalArrangement = Arrangement.spacedBy(topBarActionSpacing()),
+            verticalAlignment = Alignment.CenterVertically,
+            content = content
+        )
+        return
+    }
+
+    val counter = remember { TopBarMergeCounter() }
+    counter.index = 0
+    val capsuleShape = RoundedCornerShape(50)
+    val capsuleBg = when (style) {
+        TopBarButtonStyle.Tonal -> LegadoTheme.colorScheme.surfaceContainerLow
+        TopBarButtonStyle.Glass -> GlassTopAppBarDefaults.controlContainerColor()
+        else -> Color.Transparent // Outlined
+    }
+    Box(
+        modifier = modifier
+            .height(TopBarSeriesIconButtonSize.height)
+            .clip(capsuleShape)
+            .background(capsuleBg, capsuleShape)
+            .then(
+                if (style == TopBarButtonStyle.Outlined) {
+                    Modifier.border(1.dp, LegadoTheme.colorScheme.outlineVariant, capsuleShape)
+                } else {
+                    Modifier
+                }
+            )
+    ) {
+        CompositionLocalProvider(LocalTopBarMergeState provides counter) {
+            Row(
+                verticalAlignment = Alignment.CenterVertically,
+                content = content
+            )
+        }
+    }
+}
+
 @Composable
 @Composable
 private fun TopBarButton(
 private fun TopBarButton(
     onClick: () -> Unit,
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
     imageVector: ImageVector = Icons.AutoMirrored.Filled.ArrowBack,
     imageVector: ImageVector = Icons.AutoMirrored.Filled.ArrowBack,
-    contentDescription: String? = null
+    contentDescription: String? = null,
+    style: TopBarButtonStyle = currentTopBarButtonStyle()
 ) {
 ) {
-    FilledTonalIconButton(
-        onClick = onClick,
-        modifier = modifier.size(36.dp),
-        colors = IconButtonDefaults.filledTonalIconButtonColors(
-            containerColor = GlassTopAppBarDefaults.controlContainerColor(),
-            contentColor = LegadoTheme.colorScheme.onSurface
-        )
-    ) {
-        AnimatedIcon(
-            modifier = Modifier.size(20.dp),
-            imageVector = imageVector,
-            contentDescription = contentDescription
-        )
+    val mergeState = LocalTopBarMergeState.current
+    if (mergeState != null) {
+        val index = mergeState.index
+        mergeState.index = index + 1
+        Box(
+            contentAlignment = Alignment.Center,
+            modifier = modifier
+                .size(style.buttonSize)
+                .then(if (index > 0) Modifier.mergedDivider() else Modifier)
+                .clickable(
+                    interactionSource = remember { MutableInteractionSource() },
+                    indication = ripple(bounded = true),
+                    role = Role.Button,
+                    onClick = onClick
+                )
+        ) {
+            AppIcon(
+                imageVector = imageVector,
+                contentDescription = contentDescription,
+                tint = LegadoTheme.colorScheme.onSurface,
+                modifier = Modifier.size(style.iconSize)
+            )
+        }
+    } else {
+        val containerColor = if (style == TopBarButtonStyle.Glass) {
+            GlassTopAppBarDefaults.controlContainerColor()
+        } else {
+            null
+        }
+        SeriesButton(
+            onClick = onClick,
+            modifier = modifier,
+            enforceMinimumInteractiveSize = false,
+            size = style.buttonSize,
+            style = style.seriesStyle,
+            contentColor = LegadoTheme.colorScheme.onSurface,
+            containerColor = containerColor
+        ) { resolvedContentColor ->
+            AppIcon(
+                imageVector = imageVector,
+                contentDescription = contentDescription,
+                tint = resolvedContentColor,
+                modifier = Modifier.size(style.iconSize)
+            )
+        }
     }
     }
 }
 }
 
 
@@ -94,8 +270,6 @@ fun TopBarActionButton(
     contentDescription: String?,
     contentDescription: String?,
     modifier: Modifier = Modifier
     modifier: Modifier = Modifier
 ) {
 ) {
-    val enableProgressive =
-        LocalAppUiConfiguration.current.theme.enableProgressiveBlur
     if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
     if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
         MiuixIconButton(
         MiuixIconButton(
             onClick = onClick,
             onClick = onClick,
@@ -107,25 +281,15 @@ fun TopBarActionButton(
             )
             )
         }
         }
     } else {
     } else {
-        if (enableProgressive) {
-            TopBarButton(
-                onClick = onClick,
-                imageVector = imageVector,
-                contentDescription = contentDescription,
-                modifier = modifier
-            )
-        } else {
-            MediumPlainButton(
-                onClick = onClick,
-                modifier = modifier,
-                icon = imageVector,
-                contentDescription = contentDescription,
-            )
-        }
+        TopBarButton(
+            onClick = onClick,
+            imageVector = imageVector,
+            contentDescription = contentDescription,
+            modifier = modifier
+        )
     }
     }
 }
 }
 
 
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 @Composable
 fun TopBarAnimatedActionButton(
 fun TopBarAnimatedActionButton(
     checked: Boolean,
     checked: Boolean,
@@ -188,6 +352,20 @@ fun TopBarAnimatedActionButton(
             }
             }
         )
         )
     } else {
     } else {
+        val topBarStyle = currentTopBarButtonStyle()
+        val mergeState = LocalTopBarMergeState.current
+        val mergeIndex = if (mergeState != null) {
+            val index = mergeState.index
+            mergeState.index = index + 1
+            index
+        } else {
+            -1
+        }
+        val containerColor = if (mergeState == null && topBarStyle == TopBarButtonStyle.Glass) {
+            GlassTopAppBarDefaults.controlContainerColor()
+        } else {
+            null
+        }
         AnimatedActionButtonCore(
         AnimatedActionButtonCore(
             checked = checked,
             checked = checked,
             onCheckedChange = onCheckedChange,
             onCheckedChange = onCheckedChange,
@@ -200,17 +378,42 @@ fun TopBarAnimatedActionButton(
             textStyle = LegadoTheme.typography.labelMedium,
             textStyle = LegadoTheme.typography.labelMedium,
             textStartPadding = 8.dp,
             textStartPadding = 8.dp,
             button = { buttonModifier, onToggle, content ->
             button = { buttonModifier, onToggle, content ->
-                ToggleButton(
-                    modifier = buttonModifier,
-                    contentPadding = PaddingValues(horizontal = 8.dp),
-                    checked = checked,
-                    onCheckedChange = onToggle
-                ) {
-                    Row(
-                        verticalAlignment = Alignment.CenterVertically,
-                        horizontalArrangement = Arrangement.Center,
-                        content = content
-                    )
+                val dividerModifier = buttonModifier
+                    .then(if (mergeIndex > 0) Modifier.mergedDivider() else Modifier)
+                if (mergeIndex >= 0) {
+                    Box(
+                        contentAlignment = Alignment.Center,
+                        modifier = dividerModifier.clickable(
+                            interactionSource = remember { MutableInteractionSource() },
+                            indication = ripple(bounded = true),
+                            role = Role.Button,
+                            onClick = { onToggle(!checked) }
+                        )
+                    ) {
+                        Row(
+                            verticalAlignment = Alignment.CenterVertically,
+                            horizontalArrangement = Arrangement.Center,
+                            modifier = Modifier.padding(horizontal = 8.dp),
+                            content = content
+                        )
+                    }
+                } else {
+                    SeriesButton(
+                        onClick = { onToggle(!checked) },
+                        modifier = dividerModifier,
+                        enforceMinimumInteractiveSize = false,
+                        selected = checked,
+                        style = topBarStyle.seriesStyle,
+                        contentColor = LegadoTheme.colorScheme.onSurface,
+                        containerColor = containerColor
+                    ) {
+                        Row(
+                            verticalAlignment = Alignment.CenterVertically,
+                            horizontalArrangement = Arrangement.Center,
+                            modifier = Modifier.padding(horizontal = 8.dp),
+                            content = content
+                        )
+                    }
                 }
                 }
             },
             },
             icon = { imageVector, iconModifier, _ ->
             icon = { imageVector, iconModifier, _ ->

+ 14 - 0
app/src/main/res/values-zh-rCN/arrays.xml

@@ -223,6 +223,20 @@
         <item>双击</item>
         <item>双击</item>
     </string-array>
     </string-array>
 
 
+    <string-array name="top_bar_button_style">
+        <item>普通</item>
+        <item>填充</item>
+        <item>描边</item>
+        <item>玻璃</item>
+    </string-array>
+
+    <string-array name="top_bar_button_style_value">
+        <item>plain</item>
+        <item>tonal</item>
+        <item>outlined</item>
+        <item>glass</item>
+    </string-array>
+
     <string-array name="title_bar_mode">
     <string-array name="title_bar_mode">
         <item>在应用栏上显示</item>
         <item>在应用栏上显示</item>
         <item>在独立行上显示</item>
         <item>在独立行上显示</item>

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

@@ -1807,6 +1807,8 @@
     <string name="click_image_way">点击图片行为</string>
     <string name="click_image_way">点击图片行为</string>
     <string name="show_web_log">输出日志</string>
     <string name="show_web_log">输出日志</string>
     <string name="is_blur_progressive_enable">启用渐变模糊</string>
     <string name="is_blur_progressive_enable">启用渐变模糊</string>
+    <string name="top_bar_button_style">顶栏按钮样式</string>
+    <string name="merge_top_bar_actions">合并顶栏按钮</string>
     <string name="enable_select_vibrator">选择文本时震动</string>
     <string name="enable_select_vibrator">选择文本时震动</string>
     <string name="re_sync_webdav">重新同步远程书籍</string>
     <string name="re_sync_webdav">重新同步远程书籍</string>
     <string name="underline_padding">离字间距</string>
     <string name="underline_padding">离字间距</string>

+ 14 - 0
app/src/main/res/values/arrays.xml

@@ -226,6 +226,20 @@
 		<item>Off</item>
 		<item>Off</item>
 	</string-array>
 	</string-array>
 
 
+    <string-array name="top_bar_button_style">
+        <item>Plain</item>
+        <item>Tonal</item>
+        <item>Outlined</item>
+        <item>Glass</item>
+    </string-array>
+
+    <string-array name="top_bar_button_style_value">
+        <item>plain</item>
+        <item>tonal</item>
+        <item>outlined</item>
+        <item>glass</item>
+    </string-array>
+
     <string-array name="paletteStyle">
     <string-array name="paletteStyle">
         <item>TonalSpot</item>
         <item>TonalSpot</item>
         <item>Neutral</item>
         <item>Neutral</item>

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

@@ -1864,6 +1864,8 @@
     <string name="click_image_way">Image Tap Action</string>
     <string name="click_image_way">Image Tap Action</string>
     <string name="show_web_log">Output Logs</string>
     <string name="show_web_log">Output Logs</string>
     <string name="is_blur_progressive_enable">Enable Progressive Blur</string>
     <string name="is_blur_progressive_enable">Enable Progressive Blur</string>
+    <string name="top_bar_button_style">Top Bar Button Style</string>
+    <string name="merge_top_bar_actions">Merge Top Bar Buttons</string>
     <string name="enable_select_vibrator">Vibrate When Selecting Text</string>
     <string name="enable_select_vibrator">Vibrate When Selecting Text</string>
     <string name="re_sync_webdav">Resync Remote Books</string>
     <string name="re_sync_webdav">Resync Remote Books</string>
     <string name="underline_padding">Underline Gap</string>
     <string name="underline_padding">Underline Gap</string>

+ 7 - 3
app/src/test/java/io/legado/app/data/repository/ThemeSettingsMappingTest.kt

@@ -27,18 +27,18 @@ class ThemeSettingsMappingTest {
     }
     }
 
 
     @Test
     @Test
-    fun `Theme gateway 持久化边界固定为 67 键`() {
+    fun `Theme gateway 持久化边界固定为 69 键`() {
         val actualKeys = ThemeSettings().toGatewayPrefMap().keys
         val actualKeys = ThemeSettings().toGatewayPrefMap().keys
         val expectedKeys = ThemeSettings().expectedGatewayPrefMap().keys
         val expectedKeys = ThemeSettings().expectedGatewayPrefMap().keys
 
 
-        assertEquals(67, actualKeys.size)
+        assertEquals(69, actualKeys.size)
         assertEquals(expectedKeys, actualKeys)
         assertEquals(expectedKeys, actualKeys)
         assertFalse(PreferKey.customMode in actualKeys)
         assertFalse(PreferKey.customMode in actualKeys)
         assertFalse(PreferKey.bookInfoInputColor in actualKeys)
         assertFalse(PreferKey.bookInfoInputColor in actualKeys)
     }
     }
 
 
     @Test
     @Test
-    fun `Theme gateway 67 键写读映射逐字段对应`() {
+    fun `Theme gateway 69 键写读映射逐字段对应`() {
         themeMappingSamples().forEach { expected ->
         themeMappingSamples().forEach { expected ->
             assertEquals(expected.expectedGatewayPrefMap(), expected.toGatewayPrefMap())
             assertEquals(expected.expectedGatewayPrefMap(), expected.toGatewayPrefMap())
             assertEquals(
             assertEquals(
@@ -186,6 +186,8 @@ private fun themeMappingSamples(): List<ThemeSettings> {
         bottomBarBlurAlpha = 123,
         bottomBarBlurAlpha = 123,
         bottomBarLensRadius = 3.75f,
         bottomBarLensRadius = 3.75f,
         useFlexibleTopAppBar = false,
         useFlexibleTopAppBar = false,
+        topBarButtonStyle = "glass",
+        mergeTopBarActions = true,
         bookInfoFollowCoverColor = false,
         bookInfoFollowCoverColor = false,
         bookInfoNetworkCoverBackground = "network-background",
         bookInfoNetworkCoverBackground = "network-background",
         bookInfoDefaultCoverBackground = "default-background",
         bookInfoDefaultCoverBackground = "default-background",
@@ -277,6 +279,8 @@ private fun ThemeSettings.expectedGatewayPrefMap(): Map<String, Any?> = mapOf(
     PreferKey.bottomBarBlurAlpha to bottomBarBlurAlpha,
     PreferKey.bottomBarBlurAlpha to bottomBarBlurAlpha,
     PreferKey.bottomBarLensRadius to bottomBarLensRadius,
     PreferKey.bottomBarLensRadius to bottomBarLensRadius,
     PreferKey.useFlexibleTopAppBar to useFlexibleTopAppBar,
     PreferKey.useFlexibleTopAppBar to useFlexibleTopAppBar,
+    PreferKey.topBarButtonStyle to topBarButtonStyle,
+    PreferKey.mergeTopBarActions to mergeTopBarActions,
     PreferKey.bookInfoFollowCoverColor to bookInfoFollowCoverColor,
     PreferKey.bookInfoFollowCoverColor to bookInfoFollowCoverColor,
     PreferKey.bookInfoNetworkCoverBackground to bookInfoNetworkCoverBackground,
     PreferKey.bookInfoNetworkCoverBackground to bookInfoNetworkCoverBackground,
     PreferKey.bookInfoDefaultCoverBackground to bookInfoDefaultCoverBackground,
     PreferKey.bookInfoDefaultCoverBackground to bookInfoDefaultCoverBackground,