feat: 顶栏液态玻璃胶囊布局与合并按钮

- 合并换源/刷新/更多按钮为一个药丸形液态玻璃按钮(仅液态玻璃开启时)
- 新增标题胶囊布局开关,将书名和章节名放入导航按钮旁的液态玻璃胶囊内
- 去掉切换到 TextTitle 菜单 tab 时隐藏顶栏的逻辑
- 玻璃效果统一为 40dp,与返回按钮一致,镜头扭曲不受裁切
@
This commit is contained in:
HapeLee
2026-06-14 21:36:09 +08:00
parent 67a25c7cfe
commit 5a587222eb
9 changed files with 328 additions and 40 deletions
@@ -302,6 +302,7 @@ object PreferKey {
const val readMenuTopBarBlurMode = "readMenuTopBarBlurMode"
const val readMenuBottomBarBlurMode = "readMenuBottomBarBlurMode"
const val readMenuTopBarLiquidGlassButtons = "readMenuTopBarLiquidGlassButtons"
const val readMenuTopBarTitleCapsule = "readMenuTopBarTitleCapsule"
const val readMenuBottomBarLiquidGlassButtons = "readMenuBottomBarLiquidGlassButtons"
const val readMenuTopBarBlurStyle = "readMenuTopBarBlurStyle"
const val readMenuBottomBarBlurStyle = "readMenuBottomBarBlurStyle"
@@ -88,6 +88,7 @@ data class ReadPreferences(
val readMenuTopBarBlurMode: Int = ReadMenuBlurMode.None,
val readMenuBottomBarBlurMode: Int = ReadMenuBlurMode.None,
val readMenuTopBarLiquidGlassButtons: Boolean = false,
val readMenuTopBarTitleCapsule: Boolean = false,
val readMenuBottomBarLiquidGlassButtons: Boolean = false,
val readMenuTopBarBlurStyle: Int = ReadMenuBlurStyle.Progressive,
val readMenuBottomBarBlurStyle: Int = ReadMenuBlurStyle.Solid,
@@ -316,6 +317,9 @@ class ReadSettingsRepository(
suspend fun setReadMenuTopBarLiquidGlassButtons(value: Boolean) =
settingsRepository.putBoolean(PreferKey.readMenuTopBarLiquidGlassButtons, value)
suspend fun setReadMenuTopBarTitleCapsule(value: Boolean) =
settingsRepository.putBoolean(PreferKey.readMenuTopBarTitleCapsule, value)
suspend fun setReadMenuBottomBarLiquidGlassButtons(value: Boolean) =
settingsRepository.putBoolean(PreferKey.readMenuBottomBarLiquidGlassButtons, value)
@@ -204,6 +204,7 @@ object ReadBookConfig {
var readMenuIconShowText by prefDelegate(PreferKey.readMenuIconShowText, true)
var readMenuFloatingBottomBar by prefDelegate(PreferKey.readMenuFloatingBottomBar, false)
var readMenuTopBarLiquidGlassButtons by prefDelegate(PreferKey.readMenuTopBarLiquidGlassButtons, false)
var readMenuTopBarTitleCapsule by prefDelegate(PreferKey.readMenuTopBarTitleCapsule, false)
var readMenuBottomBarLiquidGlassButtons by prefDelegate(PreferKey.readMenuBottomBarLiquidGlassButtons, false)
var readMenuBorderColor by prefDelegate(PreferKey.readMenuBorderColor, 0)
var readMenuBorderColorNight by prefDelegate(PreferKey.readMenuBorderColorNight, 0)
@@ -187,6 +187,7 @@ data class ReadMenuConfig(
val readMenuTopBarBlurMode: Int = ReadMenuBlurMode.None,
val readMenuBottomBarBlurMode: Int = ReadMenuBlurMode.None,
val readMenuTopBarLiquidGlassButtons: Boolean = false,
val readMenuTopBarTitleCapsule: Boolean = false,
val readMenuBottomBarLiquidGlassButtons: Boolean = false,
val readMenuTopBarBlurStyle: Int = ReadMenuBlurStyle.Progressive,
val readMenuBottomBarBlurStyle: Int = ReadMenuBlurStyle.Solid,
@@ -1004,6 +1005,9 @@ sealed interface ConfigUpdate {
data class MenuTopBarLiquidGlassButtons(val value: Boolean) : ConfigUpdate {
override val actions = emptySet<ConfigUpdateAction>()
}
data class MenuTopBarTitleCapsule(val value: Boolean) : ConfigUpdate {
override val actions = emptySet<ConfigUpdateAction>()
}
data class MenuBottomBarLiquidGlassButtons(val value: Boolean) : ConfigUpdate {
override val actions = emptySet<ConfigUpdateAction>()
}
@@ -195,8 +195,7 @@ fun ReadBookMenuBar(
}
}
val hideTopBar = dialogLikeRoute ||
currentRoute == ReadBookMenuRoute.ReadStyle && readStylePage >= 1 ||
currentRoute == ReadBookMenuRoute.TextTitle
currentRoute == ReadBookMenuRoute.ReadStyle && readStylePage >= 1
val menuColors = readMenuColors()
Box(Modifier.fillMaxSize()) {
@@ -902,6 +901,8 @@ private fun MenuTitleBar(
)
)
) {
val useTitleCapsule = readMenuTopBarTitleCapsuleEnabled(backdrop, state.menuConfig)
// Title row: back + title + actions + overflow
Row(
modifier = Modifier
@@ -919,7 +920,16 @@ private fun MenuTitleBar(
backdrop = backdrop,
)
if (titleBarMode != "1" && titleBarMode != "3") {
if (useTitleCapsule) {
// Title capsule layout: title + chapter in a glass pill
TitleCapsuleGlassLayout(
state = state,
colors = colors,
onIntent = onIntent,
backdrop = backdrop,
titleTextColor = titleTextColor,
)
} else if (titleBarMode != "1" && titleBarMode != "3") {
AppText(
text = state.bookName,
modifier = Modifier
@@ -941,46 +951,55 @@ private fun MenuTitleBar(
Spacer(Modifier.weight(1f))
}
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Source action button (non-local books only)
if (!state.isLocalBook) {
SourceActionButton(
state = state,
colors = colors,
onIntent = onIntent,
backdrop = backdrop,
)
RefreshActionButton(
state = state,
colors = colors,
onIntent = onIntent,
backdrop = backdrop,
)
}
if (readMenuTopBarButtonLiquidGlassEnabled(backdrop, state.menuConfig)) {
MenuTitleBarMergedGlassButton(
state = state,
colors = colors,
onIntent = onIntent,
backdrop = backdrop,
)
} else {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Source action button (non-local books only)
if (!state.isLocalBook) {
SourceActionButton(
state = state,
colors = colors,
onIntent = onIntent,
backdrop = backdrop,
)
RefreshActionButton(
state = state,
colors = colors,
onIntent = onIntent,
backdrop = backdrop,
)
}
Box {
MenuTitleGlassButton(
onClick = { expanded = true },
icon = Icons.Default.MoreVert,
state = state,
colors = colors,
backdrop = backdrop,
)
OverflowDropdownMenu(
state = state,
onIntent = onIntent,
expanded = expanded,
onDismiss = { expanded = false },
)
Box {
MenuTitleGlassButton(
onClick = { expanded = true },
icon = Icons.Default.MoreVert,
state = state,
colors = colors,
backdrop = backdrop,
)
OverflowDropdownMenu(
state = state,
onIntent = onIntent,
expanded = expanded,
onDismiss = { expanded = false },
)
}
}
}
}
// Book name on its own line (mode "1")
if (titleBarMode == "1") {
// Book name on its own line (mode "1") — hidden when capsule is active
if (titleBarMode == "1" && !useTitleCapsule) {
AppText(
text = state.bookName,
modifier = Modifier
@@ -1000,8 +1019,8 @@ private fun MenuTitleBar(
)
}
// Chapter name + source action (modes "0" and "1")
if (titleBarMode == "0" || titleBarMode == "1") {
// Chapter name + source action (modes "0" and "1") — hidden when capsule is active
if ((titleBarMode == "0" || titleBarMode == "1") && !useTitleCapsule) {
Row(
modifier = Modifier
.fillMaxWidth()
@@ -1219,6 +1238,228 @@ private fun ReadMenuGlassButtonSurface(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun RowScope.TitleCapsuleGlassLayout(
state: ReadBookUiState,
colors: ReadMenuColors,
onIntent: (ReadBookIntent) -> Unit,
backdrop: Backdrop?,
titleTextColor: Color,
) {
val pillShape = RoundedCornerShape(50)
Row(
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)
.height(40.dp)
.readMenuLiquidGlass(
backdrop = backdrop,
colors = colors,
shape = pillShape,
useTopBarStyle = true,
useLens = false,
blurRadius = 32.dp,
menuConfig = state.menuConfig,
)
.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) }
var overflowExpanded by remember { mutableStateOf(false) }
val pillShape = RoundedCornerShape(50)
val tint = LegadoTheme.colorScheme.onSurfaceVariant
Box {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(48.dp),
) {
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,
)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// SwapHoriz - change source
if (!state.isLocalBook) {
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 },
),
) {
Icon(
imageVector = Icons.Default.SwapHoriz,
contentDescription = stringResource(R.string.change_origin),
tint = tint,
modifier = Modifier.size(20.dp),
)
}
Box(
modifier = Modifier
.width(1.dp)
.height(20.dp)
.background(tint.copy(alpha = 0.15f))
)
// Refresh
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(40.dp)
.combinedClickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
role = Role.Button,
onClick = { onIntent(ReadBookIntent.MenuRefreshAfter) },
onLongClick = { refreshExpanded = true },
),
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = stringResource(R.string.menu_refresh_after),
tint = tint,
modifier = Modifier.size(20.dp),
)
}
Box(
modifier = Modifier
.width(1.dp)
.height(20.dp)
.background(tint.copy(alpha = 0.15f))
)
}
// MoreVert - overflow menu
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(40.dp)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
role = Role.Button,
onClick = { overflowExpanded = true },
),
) {
Icon(
imageVector = Icons.Default.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.MenuChangeSource) },
)
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) },
)
}
}
OverflowDropdownMenu(
state = state,
onIntent = onIntent,
expanded = overflowExpanded,
onDismiss = { overflowExpanded = false },
)
}
}
@Composable
private fun SourceActionButton(
state: ReadBookUiState,
@@ -2406,6 +2647,15 @@ private fun readMenuTopBarButtonLiquidGlassEnabled(
readMenuLiquidGlassAvailable(backdrop)
}
private fun readMenuTopBarTitleCapsuleEnabled(
backdrop: Backdrop?,
menuConfig: ReadMenuConfig,
): Boolean {
return menuConfig.readMenuTopBarBlurMode != ReadMenuBlurMode.None &&
menuConfig.readMenuTopBarTitleCapsule &&
readMenuLiquidGlassAvailable(backdrop)
}
private fun readMenuBottomBarButtonLiquidGlassEnabled(
backdrop: Backdrop?,
menuConfig: ReadMenuConfig,
@@ -1604,6 +1604,7 @@ class ReadBookViewModel(
readMenuTopBarBlurMode = ReadBookConfig.readMenuTopBarBlurMode,
readMenuBottomBarBlurMode = ReadBookConfig.readMenuBottomBarBlurMode,
readMenuTopBarLiquidGlassButtons = ReadBookConfig.readMenuTopBarLiquidGlassButtons,
readMenuTopBarTitleCapsule = ReadBookConfig.readMenuTopBarTitleCapsule,
readMenuBottomBarLiquidGlassButtons = ReadBookConfig.readMenuBottomBarLiquidGlassButtons,
readMenuTopBarBlurStyle = ReadBookConfig.readMenuTopBarBlurStyle,
readMenuBottomBarBlurStyle = ReadBookConfig.readMenuBottomBarBlurStyle,
@@ -2763,6 +2764,16 @@ class ReadBookViewModel(
}
}
is ConfigUpdate.MenuTopBarTitleCapsule -> {
ReadBookConfig.readMenuTopBarTitleCapsule = update.value
viewModelScope.launch {
readSettingsRepository.setReadMenuTopBarTitleCapsule(update.value)
}
_uiState.update {
it.copy(menuConfig = it.menuConfig.copy(readMenuTopBarTitleCapsule = update.value))
}
}
is ConfigUpdate.MenuBottomBarLiquidGlassButtons -> {
ReadBookConfig.readMenuBottomBarLiquidGlassButtons = update.value
viewModelScope.launch {
@@ -746,6 +746,21 @@ private fun TopBarTab(
},
)
}
AnimatedVisibility(visible = topBarBlurEnabled) {
TinySwitchSettingItem(
title = stringResource(R.string.read_menu_top_bar_title_capsule),
checked = preferences.readMenuTopBarTitleCapsule,
onCheckedChange = {
onIntent(
ReadBookIntent.UpdateConfig(
ConfigUpdate.MenuTopBarTitleCapsule(
it
)
)
)
},
)
}
}
}