[新增] 悬浮底栏和液态玻璃底栏

This commit is contained in:
HapeLee
2026-04-08 02:02:50 +08:00
parent a37e1ac62e
commit e38d96a6e5
16 changed files with 990 additions and 106 deletions
@@ -207,6 +207,8 @@ object PreferKey {
const val themePref = "themePref" const val themePref = "themePref"
const val bookshelfSortOrder = "bookshelfSortOrder" const val bookshelfSortOrder = "bookshelfSortOrder"
const val showBottomView = "showBottomView" const val showBottomView = "showBottomView"
const val useFloatingBottomBar = "useFloatingBottomBar"
const val useFloatingBottomBarLiquidGlass = "useFloatingBottomBarLiquidGlass"
const val mangaScrollMode = "mangaScrollMode" const val mangaScrollMode = "mangaScrollMode"
const val webtoonSidePaddingDp = "webtoonSidePaddingDp" const val webtoonSidePaddingDp = "webtoonSidePaddingDp"
const val mangaVolumeKeyPage = "mangaVolumeKeyPage" const val mangaVolumeKeyPage = "mangaVolumeKeyPage"
@@ -267,6 +269,7 @@ object PreferKey {
const val defaultSourceChangeAll = "defaultSourceChangeAll" const val defaultSourceChangeAll = "defaultSourceChangeAll"
const val showTip = "showTip" const val showTip = "showTip"
const val showBookCount = "showBookCount"
const val sliderVibrator = "sliderVibrator" const val sliderVibrator = "sliderVibrator"
const val selectVibrator = "selectVibrator" const val selectVibrator = "selectVibrator"
const val audioCacheCleanTime = "audioCacheCleanTime" const val audioCacheCleanTime = "audioCacheCleanTime"
@@ -0,0 +1,142 @@
package io.legado.app.ui.animation
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.spring
import androidx.compose.foundation.MutatorMutex
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.unit.IntSize
import io.legado.app.ui.util.inspectDragGestures
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.android.awaitFrame
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlin.math.abs
class DampedDragAnimation(
private val animationScope: CoroutineScope,
val initialValue: Float,
val valueRange: ClosedRange<Float>,
val visibilityThreshold: Float,
val initialScale: Float,
val pressedScale: Float,
val canDrag: (Offset) -> Boolean = { true },
val onDragStarted: DampedDragAnimation.(position: Offset) -> Unit,
val onDragStopped: DampedDragAnimation.() -> Unit,
val onDrag: DampedDragAnimation.(size: IntSize, dragAmount: Offset) -> Unit,
) {
private val valueAnimationSpec = spring(1f, 1000f, visibilityThreshold)
private val velocityAnimationSpec = spring(0.5f, 300f, visibilityThreshold * 10f)
private val pressProgressAnimationSpec = spring(1f, 1000f, 0.001f)
private val scaleXAnimationSpec = spring(0.6f, 250f, 0.001f)
private val scaleYAnimationSpec = spring(0.7f, 250f, 0.001f)
private val valueAnimation = Animatable(initialValue, visibilityThreshold)
private val velocityAnimation = Animatable(0f, 5f)
private val pressProgressAnimation = Animatable(0f, 0.001f)
private val scaleXAnimation = Animatable(initialScale, 0.001f)
private val scaleYAnimation = Animatable(initialScale, 0.001f)
private val mutatorMutex = MutatorMutex()
private val velocityTracker = VelocityTracker()
val value: Float get() = valueAnimation.value
val targetValue: Float get() = valueAnimation.targetValue
val pressProgress: Float get() = pressProgressAnimation.value
val scaleX: Float get() = scaleXAnimation.value
val scaleY: Float get() = scaleYAnimation.value
val velocity: Float get() = velocityAnimation.value
val modifier: Modifier = Modifier.pointerInput(Unit) {
inspectDragGestures(
onDragStart = { down ->
onDragStarted(down.position)
press()
},
onDragEnd = {
onDragStopped()
release()
},
onDragCancel = {
onDragStopped()
release()
}
) { change, dragAmount ->
val position = change.position
val previousPosition = change.previousPosition
val isInside = canDrag(position)
val wasInside = canDrag(previousPosition)
if (isInside && wasInside) {
onDrag(size, dragAmount)
}
}
}
fun press() {
velocityTracker.resetTracking()
animationScope.launch {
launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) }
launch { scaleXAnimation.animateTo(pressedScale, scaleXAnimationSpec) }
launch { scaleYAnimation.animateTo(pressedScale, scaleYAnimationSpec) }
}
}
fun release() {
animationScope.launch {
awaitFrame()
if (value != targetValue) {
val threshold = (valueRange.endInclusive - valueRange.start) * 0.025f
snapshotFlow { valueAnimation.value }
.filter { abs(it - valueAnimation.targetValue) < threshold }
.first()
}
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
launch { scaleXAnimation.animateTo(initialScale, scaleXAnimationSpec) }
launch { scaleYAnimation.animateTo(initialScale, scaleYAnimationSpec) }
}
}
fun updateValue(value: Float) {
val coercedTargetValue = value.coerceIn(valueRange)
animationScope.launch {
launch {
valueAnimation.animateTo(coercedTargetValue, valueAnimationSpec) {
updateVelocity()
}
}
}
}
fun animateToValue(value: Float) {
animationScope.launch {
mutatorMutex.mutate {
press()
val coercedTargetValue = value.coerceIn(valueRange)
launch { valueAnimation.animateTo(coercedTargetValue, valueAnimationSpec) }
if (velocity != 0f) {
launch { velocityAnimation.animateTo(0f, velocityAnimationSpec) }
}
release()
}
}
}
private fun updateVelocity() {
velocityTracker.addPosition(
System.currentTimeMillis(),
Offset(value, 0f)
)
val targetVelocity =
velocityTracker.calculateVelocity().x / (valueRange.endInclusive - valueRange.start)
animationScope.launch {
velocityAnimation.animateTo(targetVelocity, velocityAnimationSpec)
}
}
}
@@ -0,0 +1,109 @@
package io.legado.app.ui.animation
import android.annotation.SuppressLint
import android.graphics.RuntimeShader
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.spring
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.util.fastCoerceIn
import io.legado.app.ui.util.inspectDragGestures
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.intellij.lang.annotations.Language
@SuppressLint("NewApi")
class InteractiveHighlight(
val animationScope: CoroutineScope,
val position: (size: Size, offset: Offset) -> Offset = { _, offset -> offset }
) {
private val pressProgressAnimationSpec = spring(0.5f, 300f, 0.001f)
private val positionAnimationSpec = spring(0.5f, 300f, Offset.VisibilityThreshold)
private val pressProgressAnimation = Animatable(0f, 0.001f)
private val positionAnimation =
Animatable(Offset.Zero, Offset.VectorConverter, Offset.VisibilityThreshold)
private var startPosition = Offset.Zero
@Language("AGSL")
private val shader = RuntimeShader(
"""
uniform float2 size;
layout(color) uniform half4 color;
uniform float radius;
uniform float2 position;
half4 main(float2 coord) {
float dist = distance(coord, position);
float intensity = smoothstep(radius, radius * 0.5, dist);
return color * intensity;
}
"""
)
val modifier: Modifier =
Modifier.drawWithContent {
val progress = pressProgressAnimation.value
if (progress > 0f) {
drawRect(
Color.White.copy(0.06f * progress),
blendMode = BlendMode.Plus
)
shader.apply {
val currentPosition = position(size, positionAnimation.value)
setFloatUniform("size", size.width, size.height)
setColorUniform("color", Color.White.copy(0.12f * progress).toArgb())
setFloatUniform("radius", size.minDimension * 1.2f)
setFloatUniform(
"position",
currentPosition.x.fastCoerceIn(0f, size.width),
currentPosition.y.fastCoerceIn(0f, size.height)
)
}
drawRect(
ShaderBrush(shader),
blendMode = BlendMode.Plus
)
}
drawContent()
}
val gestureModifier: Modifier =
Modifier.pointerInput(animationScope) {
inspectDragGestures(
onDragStart = { down ->
startPosition = down.position
animationScope.launch {
launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) }
launch { positionAnimation.snapTo(startPosition) }
}
},
onDragEnd = {
animationScope.launch {
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) }
}
},
onDragCancel = {
animationScope.launch {
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) }
}
}
) { change, _ ->
animationScope.launch { positionAnimation.snapTo(change.position) }
}
}
}
@@ -7,6 +7,11 @@ object MainConfig {
var showDiscovery by prefDelegate(PreferKey.showDiscovery, true) var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
var showRSS by prefDelegate(PreferKey.showRss, true) var showRSS by prefDelegate(PreferKey.showRss, true)
var showBottomView by prefDelegate(PreferKey.showBottomView, true) var showBottomView by prefDelegate(PreferKey.showBottomView, true)
var useFloatingBottomBar by prefDelegate(PreferKey.useFloatingBottomBar, false)
var useFloatingBottomBarLiquidGlass by prefDelegate(
PreferKey.useFloatingBottomBarLiquidGlass,
false
)
var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf") var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf")
var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto") var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto")
var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto") var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto")
@@ -72,6 +72,13 @@ object ThemeConfig {
var showBottomView by prefDelegate(PreferKey.showBottomView, true) var showBottomView by prefDelegate(PreferKey.showBottomView, true)
var useFloatingBottomBar by prefDelegate(PreferKey.useFloatingBottomBar, false)
var useFloatingBottomBarLiquidGlass by prefDelegate(
PreferKey.useFloatingBottomBarLiquidGlass,
false
)
var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto") var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto")
var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto") var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto")
@@ -35,7 +35,6 @@ import androidx.compose.material.icons.filled.BrightnessMedium
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.LightMode import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
@@ -43,10 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.ToggleButton import androidx.compose.material3.ToggleButton
import androidx.compose.material3.ToggleButtonDefaults import androidx.compose.material3.ToggleButtonDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -375,6 +371,20 @@ fun ThemeConfigScreen(
checked = ThemeConfig.showBottomView, checked = ThemeConfig.showBottomView,
onCheckedChange = { ThemeConfig.showBottomView = it } onCheckedChange = { ThemeConfig.showBottomView = it }
) )
SwitchSettingItem(
title = stringResource(R.string.floating_bottom_bar),
description = stringResource(R.string.floating_bottom_bar_summary),
checked = ThemeConfig.useFloatingBottomBar,
onCheckedChange = { ThemeConfig.useFloatingBottomBar = it }
)
AnimatedVisibility(visible = ThemeConfig.useFloatingBottomBar) {
SwitchSettingItem(
title = stringResource(R.string.floating_bottom_bar_liquid_glass),
description = stringResource(R.string.floating_bottom_bar_liquid_glass_summary),
checked = ThemeConfig.useFloatingBottomBarLiquidGlass,
onCheckedChange = { ThemeConfig.useFloatingBottomBarLiquidGlass = it }
)
}
DropdownListSettingItem( DropdownListSettingItem(
title = stringResource(R.string.tabletInterface), title = stringResource(R.string.tabletInterface),
selectedValue = ThemeConfig.tabletInterface, selectedValue = ThemeConfig.tabletInterface,
@@ -1,65 +1,30 @@
package io.legado.app.ui.main package io.legado.app.ui.main
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
import androidx.compose.material.icons.filled.Explore
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.RssFeed
import androidx.compose.material.icons.outlined.Explore
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.RssFeed
import androidx.compose.ui.graphics.vector.ImageVector
import io.legado.app.R import io.legado.app.R
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Album
import top.yukonga.miuix.kmp.icon.extended.ContactsBook
import top.yukonga.miuix.kmp.icon.extended.Favorites
import top.yukonga.miuix.kmp.icon.extended.Settings
sealed class MainDestination( sealed class MainDestination(
val route: String, val route: String,
@StringRes val labelId: Int, @StringRes val labelId: Int
val m3Icon: ImageVector,
val m3SelectedIcon: ImageVector,
val miuixIcon: ImageVector,
val miuixSelectedIcon: ImageVector
) { ) {
object Bookshelf : MainDestination( object Bookshelf : MainDestination(
route = "bookshelf", route = "bookshelf",
labelId = R.string.bookshelf, labelId = R.string.bookshelf
m3Icon = Icons.AutoMirrored.Outlined.LibraryBooks,
m3SelectedIcon = Icons.AutoMirrored.Filled.LibraryBooks,
miuixIcon = MiuixIcons.Regular.ContactsBook, // 替换为实际的 MIUIX 线框图标
miuixSelectedIcon = MiuixIcons.Heavy.ContactsBook // 替换为实际的 MIUIX 填充图标
) )
object Explore : MainDestination( object Explore : MainDestination(
route = "explore", route = "explore",
labelId = R.string.discovery, labelId = R.string.discovery
m3Icon = Icons.Outlined.Explore,
m3SelectedIcon = Icons.Default.Explore,
miuixIcon = MiuixIcons.Regular.Album,
miuixSelectedIcon = MiuixIcons.Heavy.Album
) )
object Rss : MainDestination( object Rss : MainDestination(
route = "rss", route = "rss",
labelId = R.string.rss, labelId = R.string.rss
m3Icon = Icons.Outlined.RssFeed,
m3SelectedIcon = Icons.Default.RssFeed,
miuixIcon = MiuixIcons.Regular.Favorites,
miuixSelectedIcon = MiuixIcons.Heavy.Favorites
) )
object My : MainDestination( object My : MainDestination(
route = "my", route = "my",
labelId = R.string.my, labelId = R.string.my
m3Icon = Icons.Outlined.Person,
m3SelectedIcon = Icons.Default.Person,
miuixIcon = MiuixIcons.Regular.Settings,
miuixSelectedIcon = MiuixIcons.Heavy.Settings
) )
companion object { companion object {
@@ -1,14 +1,21 @@
package io.legado.app.ui.main package io.legado.app.ui.main
import android.os.Build
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
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.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
@@ -39,15 +46,18 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeSource
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
@@ -57,11 +67,17 @@ import io.legado.app.ui.main.bookshelf.BookshelfViewModel
import io.legado.app.ui.main.explore.ExploreScreen import io.legado.app.ui.main.explore.ExploreScreen
import io.legado.app.ui.main.my.MyScreen import io.legado.app.ui.main.my.MyScreen
import io.legado.app.ui.main.rss.RssScreen import io.legado.app.ui.main.rss.RssScreen
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.regularHazeEffect import io.legado.app.ui.theme.regularHazeEffect
import io.legado.app.ui.theme.responsiveHazeSource
import io.legado.app.ui.widget.components.AppNavigationBar import io.legado.app.ui.widget.components.AppNavigationBar
import io.legado.app.ui.widget.components.AppNavigationBarItem import io.legado.app.ui.widget.components.AppNavigationBarItem
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.FloatingBottomBar
import io.legado.app.ui.widget.components.FloatingBottomBarItem
import io.legado.app.ui.widget.components.GlassDefaults import io.legado.app.ui.widget.components.GlassDefaults
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.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
@@ -88,6 +104,11 @@ fun MainScreen(
val bookshelfUiState by bookshelfViewModel.uiState.collectAsState() val bookshelfUiState by bookshelfViewModel.uiState.collectAsState()
val hazeState = remember { HazeState() } val hazeState = remember { HazeState() }
val floatingBarSurfaceColor = MaterialTheme.colorScheme.surface
val floatingBarBackdrop = rememberLayerBackdrop {
drawRect(floatingBarSurfaceColor)
drawContent()
}
val destinations = remember(MainConfig.showDiscovery, MainConfig.showRSS) { val destinations = remember(MainConfig.showDiscovery, MainConfig.showRSS) {
MainDestination.mainDestinations.filter { MainDestination.mainDestinations.filter {
when (it) { when (it) {
@@ -103,6 +124,15 @@ fun MainScreen(
if (index != -1) index else 0 if (index != -1) index else 0
} }
val pagerState = rememberPagerState(initialPage = initialPage) { destinations.size } val pagerState = rememberPagerState(initialPage = initialPage) { destinations.size }
val labelVisibilityMode = MainConfig.labelVisibilityMode
val isUnlabeled = labelVisibilityMode == "unlabeled"
val useFloatingBottomBar =
!useRail && MainConfig.showBottomView && MainConfig.useFloatingBottomBar
val useLiquidGlass = useFloatingBottomBar &&
MainConfig.useFloatingBottomBarLiquidGlass &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
val alwaysShowLabel = labelVisibilityMode == "labeled"
val showLabel = !isUnlabeled
val navState = rememberWideNavigationRailState( val navState = rememberWideNavigationRailState(
initialValue = if (MainConfig.navExtended) initialValue = if (MainConfig.navExtended)
@@ -230,51 +260,116 @@ fun MainScreen(
} }
AppScaffold( AppScaffold(
modifier = Modifier.weight(1f), modifier = Modifier
.weight(1f)
.responsiveHazeSource(hazeState),
bottomBar = { bottomBar = {
if (!useRail && MainConfig.showBottomView) { if (!useRail && MainConfig.showBottomView) {
val labelVisibilityMode = MainConfig.labelVisibilityMode if (useFloatingBottomBar) {
val isUnlabeled = labelVisibilityMode == "unlabeled" Box(modifier = Modifier.fillMaxWidth()) {
FloatingBottomBar(
AppNavigationBar( modifier = Modifier
modifier = Modifier .align(Alignment.BottomCenter)
.regularHazeEffect(state = hazeState) .clickable(
.height(if (isUnlabeled) 64.dp else 80.dp) interactionSource = remember { MutableInteractionSource() },
) { indication = null,
val alwaysShowLabel = when (labelVisibilityMode) { onClick = {}
"labeled" -> true
else -> false
}
destinations.forEachIndexed { index, destination ->
val selected = pagerState.targetPage == index
AppNavigationBarItem(
selected = selected,
onClick = {
coroutineScope.launch { pagerState.animateScrollToPage(index) }
},
labelString = stringResource(destination.labelId),
miuixIcon = if (selected) destination.miuixSelectedIcon else destination.miuixIcon,
m3Icon = {
NavigationIcon(
destination = destination,
selected = selected,
upBooksCount = uiState.upBooksCount
) )
.padding(
start = 16.dp,
end = 16.dp,
bottom = 12.dp + WindowInsets.navigationBars
.asPaddingValues()
.calculateBottomPadding()
),
selectedIndex = { pagerState.currentPage },
onSelected = { index ->
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
}, },
m3IndicatorColor = GlassDefaults.glassColor( backdrop = floatingBarBackdrop,
noBlurColor = MaterialTheme.colorScheme.secondaryContainer, tabsCount = destinations.size,
blurAlpha = GlassDefaults.ThickBlurAlpha isBlurEnabled = useLiquidGlass
), ) {
m3ShowLabel = !isUnlabeled, destinations.forEachIndexed { index, destination ->
m3AlwaysShowLabel = alwaysShowLabel val selected = pagerState.currentPage == index
) FloatingBottomBarItem(
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
modifier = Modifier.defaultMinSize(minWidth = 76.dp)
) {
NavigationIcon(
destination = destination,
selected = selected,
upBooksCount = uiState.upBooksCount
)
if (showLabel && (alwaysShowLabel || selected)) {
Text(
text = stringResource(destination.labelId),
style = MaterialTheme.typography.labelSmall,
color = if (selected) {
LegadoTheme.colorScheme.primary
} else {
LegadoTheme.colorScheme.onSurface
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
}
} else {
AppNavigationBar(
modifier = Modifier
.regularHazeEffect(state = hazeState)
.height(if (isUnlabeled) 64.dp else 80.dp)
) {
destinations.forEachIndexed { index, destination ->
val selected = pagerState.targetPage == index
AppNavigationBarItem(
selected = selected,
onClick = {
coroutineScope.launch { pagerState.animateScrollToPage(index) }
},
labelString = stringResource(destination.labelId),
iconVector = AppIcons.mainDestination(destination, selected),
m3Icon = {
NavigationIcon(
destination = destination,
selected = selected,
upBooksCount = uiState.upBooksCount
)
},
m3IndicatorColor = GlassDefaults.glassColor(
noBlurColor = MaterialTheme.colorScheme.secondaryContainer,
blurAlpha = GlassDefaults.ThickBlurAlpha
),
m3ShowLabel = showLabel,
m3AlwaysShowLabel = alwaysShowLabel
)
}
} }
} }
} }
}, },
contentWindowInsets = WindowInsets(0) contentWindowInsets = WindowInsets(0)
) { _ -> ) { _ ->
Box(modifier = Modifier.hazeSource(hazeState)) { Box(
modifier = Modifier
.then(
if (useFloatingBottomBar) {
Modifier.layerBackdrop(floatingBarBackdrop)
} else {
Modifier
}
)
) {
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -319,14 +414,14 @@ private fun NavigationIcon(
upBooksCount: Int, upBooksCount: Int,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val icon = if (selected) destination.m3SelectedIcon else destination.m3Icon val icon = AppIcons.mainDestination(destination, selected)
Box(modifier = modifier) { Box(modifier = modifier) {
if (destination == MainDestination.Bookshelf && upBooksCount > 0) { if (destination == MainDestination.Bookshelf && upBooksCount > 0) {
BadgedBox(badge = { Badge { Text(upBooksCount.toString()) } }) { BadgedBox(badge = { Badge { Text(upBooksCount.toString()) } }) {
Icon(icon, contentDescription = null) AppIcon(icon, contentDescription = null)
} }
} else { } else {
Icon(icon, contentDescription = null) AppIcon(icon, contentDescription = null)
} }
} }
} }
@@ -0,0 +1,82 @@
package io.legado.app.ui.util
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.util.fastFirstOrNull
suspend fun PointerInputScope.inspectDragGestures(
onDragStart: (down: PointerInputChange) -> Unit = {},
onDragEnd: (change: PointerInputChange) -> Unit = {},
onDragCancel: () -> Unit = {},
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) {
awaitEachGesture {
val initialDown = awaitFirstDown(false, PointerEventPass.Initial)
val down = awaitFirstDown(false)
onDragStart(down)
onDrag(initialDown, Offset.Zero)
val upEvent = drag(
pointerId = initialDown.id,
onDrag = { onDrag(it, it.positionChange()) }
)
if (upEvent == null) {
onDragCancel()
} else {
onDragEnd(upEvent)
}
}
}
private suspend inline fun AwaitPointerEventScope.drag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit
): PointerInputChange? {
val isPointerUp = currentEvent.changes.fastFirstOrNull { it.id == pointerId }?.pressed != true
if (isPointerUp) {
return null
}
var pointer = pointerId
while (true) {
val change = awaitDragOrUp(pointer) ?: return null
if (change.isConsumed) {
return null
}
if (change.changedToUpIgnoreConsumed()) {
return change
}
onDrag(change)
pointer = change.id
}
}
private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
pointerId: PointerId
): PointerInputChange? {
var pointer = pointerId
while (true) {
val event = awaitPointerEvent()
val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
if (dragEvent.changedToUpIgnoreConsumed()) {
val otherDown = event.changes.fastFirstOrNull { it.pressed }
if (otherDown == null) {
return dragEvent
} else {
pointer = otherDown.id
}
} else {
val hasDragged = dragEvent.previousPosition != dragEvent.position
if (hasDragged) {
return dragEvent
}
}
}
}
@@ -61,7 +61,7 @@ fun RowScope.AppNavigationBarItem(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
labelString: String, labelString: String,
miuixIcon: ImageVector, iconVector: ImageVector,
m3Icon: @Composable () -> Unit, m3Icon: @Composable () -> Unit,
m3IndicatorColor: Color, m3IndicatorColor: Color,
m3ShowLabel: Boolean, m3ShowLabel: Boolean,
@@ -73,7 +73,7 @@ fun RowScope.AppNavigationBarItem(
MiuixNavigationBarItem( MiuixNavigationBarItem(
selected = selected, selected = selected,
onClick = onClick, onClick = onClick,
icon = miuixIcon, icon = iconVector,
label = labelString, label = labelString,
modifier = modifier modifier = modifier
) )
@@ -0,0 +1,419 @@
// Portions of this file are derived from weishu/KernelSU
// (https://github.com/tiann/KernelSU)
// Copyright (C) KernelSU contributors
// Licensed under GPL-3.0
package io.legado.app.ui.widget.components
import android.os.Build
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable
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.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastRoundToInt
import androidx.compose.ui.util.lerp
import com.kyant.backdrop.Backdrop
import com.kyant.backdrop.backdrops.layerBackdrop
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.effects.vibrancy
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.animation.InteractiveHighlight
import io.legado.app.ui.theme.LegadoTheme
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.sign
val LocalFloatingBottomBarTabScale = staticCompositionLocalOf { { 1f } }
@Composable
fun RowScope.FloatingBottomBarItem(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
val scale = LocalFloatingBottomBarTabScale.current
Column(
modifier
.clip(ContinuousCapsule)
.clickable(
interactionSource = null,
indication = null,
role = Role.Tab,
onClick = onClick
)
.fillMaxHeight()
.weight(1f)
.graphicsLayer {
val currentScale = scale()
scaleX = currentScale
scaleY = currentScale
},
verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
content = content
)
}
@Composable
fun FloatingBottomBar(
modifier: Modifier = Modifier,
selectedIndex: () -> Int,
onSelected: (index: Int) -> Unit,
backdrop: Backdrop,
tabsCount: Int,
isBlurEnabled: Boolean = true,
content: @Composable RowScope.() -> Unit
) {
val isInLightTheme = !LegadoTheme.isDark
val accentColor = LegadoTheme.colorScheme.primary
val containerColor = if (isBlurEnabled) {
LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0.4f)
} else {
LegadoTheme.colorScheme.surfaceContainer
}
val tabsBackdrop = rememberLayerBackdrop()
val density = LocalDensity.current
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val animationScope = rememberCoroutineScope()
var tabWidthPx by remember { mutableFloatStateOf(0f) }
var totalWidthPx by remember { mutableFloatStateOf(0f) }
val offsetAnimation = remember { Animatable(0f) }
val panelOffset by remember(density) {
derivedStateOf {
if (totalWidthPx == 0f) {
0f
} else {
val fraction = (offsetAnimation.value / totalWidthPx).fastCoerceIn(-1f, 1f)
with(density) {
4f.dp.toPx() * fraction.sign * EaseOut.transform(abs(fraction))
}
}
}
}
var currentIndex by remember(selectedIndex) { mutableIntStateOf(selectedIndex()) }
class DampedDragAnimationHolder {
var instance: DampedDragAnimation? = null
}
val holder = remember { DampedDragAnimationHolder() }
val dampedDragAnimation = remember(animationScope, tabsCount, density, isLtr) {
DampedDragAnimation(
animationScope = animationScope,
initialValue = selectedIndex().toFloat(),
valueRange = 0f..(tabsCount - 1).toFloat(),
visibilityThreshold = 0.001f,
initialScale = 1f,
pressedScale = 78f / 56f,
canDrag = { offset ->
val anim = holder.instance ?: return@DampedDragAnimation true
if (tabWidthPx == 0f) return@DampedDragAnimation false
val currentValue = anim.value
val indicatorX = currentValue * tabWidthPx
val padding = with(density) { 4.dp.toPx() }
val globalTouchX = if (isLtr) {
val touchX = indicatorX + offset.x
padding + touchX
} else {
totalWidthPx - padding - tabWidthPx - indicatorX + offset.x
}
globalTouchX in 0f..totalWidthPx
},
onDragStarted = {},
onDragStopped = {
val targetIndex = targetValue.fastRoundToInt().fastCoerceIn(0, tabsCount - 1)
currentIndex = targetIndex
animateToValue(targetIndex.toFloat())
animationScope.launch {
offsetAnimation.animateTo(0f, spring(1f, 300f, 0.5f))
}
},
onDrag = { _, dragAmount ->
if (tabWidthPx > 0f) {
updateValue(
(targetValue + dragAmount.x / tabWidthPx * if (isLtr) 1f else -1f)
.fastCoerceIn(0f, (tabsCount - 1).toFloat())
)
animationScope.launch {
offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x)
}
}
}
).also { holder.instance = it }
}
LaunchedEffect(selectedIndex) {
snapshotFlow { selectedIndex() }.collectLatest { currentIndex = it }
}
LaunchedEffect(dampedDragAnimation) {
snapshotFlow { currentIndex }.drop(1).collectLatest { index ->
dampedDragAnimation.animateToValue(index.toFloat())
onSelected(index)
}
}
val interactiveHighlight =
if (isBlurEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
remember(animationScope, tabWidthPx) {
InteractiveHighlight(
animationScope = animationScope,
position = { size, _ ->
Offset(
if (isLtr) {
(dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset
} else {
size.width - (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset
},
size.height / 2f
)
}
)
}
} else {
null
}
Box(
modifier = modifier.width(IntrinsicSize.Min),
contentAlignment = Alignment.CenterStart
) {
Row(
Modifier
.onGloballyPositioned { coords ->
totalWidthPx = coords.size.width.toFloat()
val contentWidthPx = totalWidthPx - with(density) { 8.dp.toPx() }
tabWidthPx = contentWidthPx / tabsCount
}
.graphicsLayer { translationX = panelOffset }
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {}
)
.drawBackdrop(
backdrop = backdrop,
shape = { ContinuousCapsule },
effects = {
if (isBlurEnabled) {
vibrancy()
blur(8f.dp.toPx())
lens(24f.dp.toPx(), 24f.dp.toPx())
}
},
highlight = {
Highlight.Default.copy(alpha = if (isBlurEnabled) 1f else 0f)
},
shadow = {
Shadow.Default.copy(
color = Color.Black.copy(if (isInLightTheme) 0.1f else 0.2f)
)
},
layerBlock = {
if (isBlurEnabled) {
val progress = dampedDragAnimation.pressProgress
val scale = lerp(1f, 1f + 16f.dp.toPx() / size.width, progress)
scaleX = scale
scaleY = scale
}
},
onDrawSurface = { drawRect(containerColor) }
)
.then(
if (isBlurEnabled && interactiveHighlight != null) {
interactiveHighlight.modifier
} else {
Modifier
}
)
.height(64.dp)
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically,
content = content
)
CompositionLocalProvider(
LocalFloatingBottomBarTabScale provides {
if (isBlurEnabled) {
lerp(1f, 1.2f, dampedDragAnimation.pressProgress)
} else {
1f
}
}
) {
Row(
Modifier
.clearAndSetSemantics {}
.alpha(0f)
.layerBackdrop(tabsBackdrop)
.graphicsLayer { translationX = panelOffset }
.drawBackdrop(
backdrop = backdrop,
shape = { ContinuousCapsule },
effects = {
if (isBlurEnabled) {
val progress = dampedDragAnimation.pressProgress
vibrancy()
blur(8f.dp.toPx())
lens(24f.dp.toPx() * progress, 24f.dp.toPx() * progress)
}
},
highlight = {
Highlight.Default.copy(
alpha = if (isBlurEnabled) {
dampedDragAnimation.pressProgress
} else {
0f
}
)
},
onDrawSurface = { drawRect(containerColor) }
)
.then(
if (isBlurEnabled && interactiveHighlight != null) {
interactiveHighlight.modifier
} else {
Modifier
}
)
.height(56.dp)
.padding(horizontal = 4.dp)
.graphicsLayer(colorFilter = ColorFilter.tint(accentColor)),
verticalAlignment = Alignment.CenterVertically,
content = content
)
}
if (tabWidthPx > 0f) {
Box(
Modifier
.padding(horizontal = 4.dp)
.graphicsLayer {
val contentWidth = totalWidthPx - with(density) { 8.dp.toPx() }
val singleTabWidth = contentWidth / tabsCount
val progressOffset = dampedDragAnimation.value * singleTabWidth
translationX = if (isLtr) {
progressOffset + panelOffset
} else {
-progressOffset + panelOffset
}
}
.then(
if (isBlurEnabled && interactiveHighlight != null) {
interactiveHighlight.gestureModifier
} else {
Modifier
}
)
.then(dampedDragAnimation.modifier)
.drawBackdrop(
backdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop),
shape = { ContinuousCapsule },
effects = {
if (isBlurEnabled) {
val progress = dampedDragAnimation.pressProgress
lens(10f.dp.toPx() * progress, 14f.dp.toPx() * progress, true)
}
},
highlight = {
Highlight.Default.copy(
alpha = if (isBlurEnabled) {
dampedDragAnimation.pressProgress
} else {
0f
}
)
},
shadow = {
Shadow(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f)
},
innerShadow = {
InnerShadow(
radius = 8f.dp * dampedDragAnimation.pressProgress,
alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f
)
},
layerBlock = {
if (isBlurEnabled) {
scaleX = dampedDragAnimation.scaleX
scaleY = dampedDragAnimation.scaleY
val velocity = dampedDragAnimation.velocity / 10f
scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f)
scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f)
}
},
onDrawSurface = {
val progress =
if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f
drawRect(
color = if (isInLightTheme) {
Color.Black.copy(0.1f)
} else {
Color.White.copy(0.1f)
},
alpha = 1f - progress
)
drawRect(Color.Black.copy(alpha = 0.03f * progress))
}
)
.height(56.dp)
.width(with(density) { ((totalWidthPx - 8.dp.toPx()) / tabsCount).toDp() })
)
}
}
}
@@ -2,22 +2,32 @@ package io.legado.app.ui.widget.components.icon
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.material.icons.automirrored.filled.LibraryBooks
import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Explore
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Replay import androidx.compose.material.icons.filled.Replay
import androidx.compose.material.icons.filled.RssFeed
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.outlined.Explore
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.RssFeed
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import io.legado.app.ui.main.MainDestination
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.basic.Search import top.yukonga.miuix.kmp.icon.basic.Search
import top.yukonga.miuix.kmp.icon.extended.Album
import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.icon.extended.Back
import top.yukonga.miuix.kmp.icon.extended.Close import top.yukonga.miuix.kmp.icon.extended.Close
import top.yukonga.miuix.kmp.icon.extended.ContactsBook
import top.yukonga.miuix.kmp.icon.extended.Favorites
import top.yukonga.miuix.kmp.icon.extended.Refresh import top.yukonga.miuix.kmp.icon.extended.Refresh
import top.yukonga.miuix.kmp.icon.extended.Settings
/**
* 应用全局图标映射层
*/
object AppIcons { object AppIcons {
private val isMiuix: Boolean private val isMiuix: Boolean
@@ -26,29 +36,46 @@ object AppIcons {
val Search: ImageVector val Search: ImageVector
@Composable @Composable
get() = if (isMiuix) { get() = if (isMiuix) MiuixIcons.Basic.Search else Icons.Default.Search
MiuixIcons.Basic.Search
} else {
Icons.Default.Search
}
val Close: ImageVector val Close: ImageVector
@Composable @Composable
get() = if (isMiuix) { get() = if (isMiuix) MiuixIcons.Close else Icons.Default.Clear
MiuixIcons.Close
} else {
Icons.Default.Clear
}
val Back: ImageVector val Back: ImageVector
@Composable @Composable
get() = if (isMiuix) { get() = if (isMiuix) MiuixIcons.Back else Icons.AutoMirrored.Filled.ArrowBack
MiuixIcons.Back
} else {
Icons.AutoMirrored.Filled.ArrowBack
}
val Replay: ImageVector val Replay: ImageVector
@Composable @Composable
get() = if (isMiuix) MiuixIcons.Refresh ?: Icons.Default.Replay else Icons.Default.Replay get() = if (isMiuix) MiuixIcons.Refresh ?: Icons.Default.Replay else Icons.Default.Replay
@Composable
fun mainDestination(destination: MainDestination, selected: Boolean): ImageVector {
return when (destination) {
MainDestination.Bookshelf -> if (isMiuix) {
if (selected) MiuixIcons.Heavy.ContactsBook else MiuixIcons.Regular.ContactsBook
} else {
if (selected) Icons.AutoMirrored.Filled.LibraryBooks else Icons.AutoMirrored.Outlined.LibraryBooks
}
MainDestination.Explore -> if (isMiuix) {
if (selected) MiuixIcons.Heavy.Album else MiuixIcons.Regular.Album
} else {
if (selected) Icons.Default.Explore else Icons.Outlined.Explore
}
MainDestination.Rss -> if (isMiuix) {
if (selected) MiuixIcons.Heavy.Favorites else MiuixIcons.Regular.Favorites
} else {
if (selected) Icons.Default.RssFeed else Icons.Outlined.RssFeed
}
MainDestination.My -> if (isMiuix) {
if (selected) MiuixIcons.Heavy.Settings else MiuixIcons.Regular.Settings
} else {
if (selected) Icons.Default.Person else Icons.Outlined.Person
}
}
}
} }
@@ -325,6 +325,7 @@
<string name="jf_convert_j">繁转简</string> <string name="jf_convert_j">繁转简</string>
<string name="page_mode">翻页模式</string> <string name="page_mode">翻页模式</string>
<string name="nb_file_sub_count">%1$d 项</string> <string name="nb_file_sub_count">%1$d 项</string>
<string name="book_count">%1$d 本</string>
<string name="nb_file_path">存储卡:</string> <string name="nb_file_path">存储卡:</string>
<string name="nb_file_add_shelf">加入书架</string> <string name="nb_file_add_shelf">加入书架</string>
<string name="nb_file_add_shelves">加入书架(%1$d</string> <string name="nb_file_add_shelves">加入书架(%1$d</string>
@@ -577,6 +578,7 @@
<string name="skip_next">下一个</string> <string name="skip_next">下一个</string>
<string name="music">音乐</string> <string name="music">音乐</string>
<string name="audio">音频</string> <string name="audio">音频</string>
<string name="web_file">网页文件</string>
<string name="is_enable">启用</string> <string name="is_enable">启用</string>
<string name="enable_js">启用 JavaScript</string> <string name="enable_js">启用 JavaScript</string>
<string name="load_with_base_url">加载 BaseUrl</string> <string name="load_with_base_url">加载 BaseUrl</string>
@@ -1293,6 +1295,10 @@
<string name="custom_mode">自定义模式</string> <string name="custom_mode">自定义模式</string>
<string name="be_swiped">仍可滑动切换页面</string> <string name="be_swiped">仍可滑动切换页面</string>
<string name="show_bottom_nav">显示导航栏</string> <string name="show_bottom_nav">显示导航栏</string>
<string name="floating_bottom_bar">悬浮底栏</string>
<string name="floating_bottom_bar_summary">底部导航栏使用悬浮胶囊样式</string>
<string name="floating_bottom_bar_liquid_glass">液态玻璃效果</string>
<string name="floating_bottom_bar_liquid_glass_summary">为悬浮底栏启用液态玻璃高光效果,需要 Android 13 及以上系统</string>
<string name="nav_label_mode">导航栏标签显示方式</string> <string name="nav_label_mode">导航栏标签显示方式</string>
<string name="read">阅读</string> <string name="read">阅读</string>
<string name="privacy">隐私</string> <string name="privacy">隐私</string>
@@ -1372,6 +1378,7 @@
<string name="read_change_all">换源按钮默认弹出整书换源</string> <string name="read_change_all">换源按钮默认弹出整书换源</string>
<string name="read_change_all_s">反之则默认弹出单章换源</string> <string name="read_change_all_s">反之则默认弹出单章换源</string>
<string name="show_tip">显示书籍类型</string> <string name="show_tip">显示书籍类型</string>
<string name="show_book_count">显示分组书籍数量</string>
<string name="is_reading">在读</string> <string name="is_reading">在读</string>
<string name="is_unread">未读</string> <string name="is_unread">未读</string>
<string name="is_read_finished">已读</string> <string name="is_read_finished">已读</string>
@@ -302,6 +302,7 @@
<string name="jf_convert_j">繁轉簡</string> <string name="jf_convert_j">繁轉簡</string>
<string name="page_mode">翻頁模式</string> <string name="page_mode">翻頁模式</string>
<string name="nb_file_sub_count">%1$d 項</string> <string name="nb_file_sub_count">%1$d 項</string>
<string name="book_count">%1$d 本</string>
<string name="nb_file_path">存儲咭:</string> <string name="nb_file_path">存儲咭:</string>
<string name="nb_file_add_shelf">加入書架</string> <string name="nb_file_add_shelf">加入書架</string>
<string name="nb_file_add_shelves">加入書架 (%1$d)</string> <string name="nb_file_add_shelves">加入書架 (%1$d)</string>
@@ -1220,4 +1221,6 @@
<string name="top_bar_opacity_summary">%d</string> <string name="top_bar_opacity_summary">%d</string>
<string name="bottom_bar_opacity">底欄不透明度</string> <string name="bottom_bar_opacity">底欄不透明度</string>
<string name="bottom_bar_opacity_summary">%d</string> <string name="bottom_bar_opacity_summary">%d</string>
<string name="web_file">網頁文件</string>
<string name="show_book_count">顯示分組書籍數量</string>
</resources> </resources>
@@ -304,6 +304,7 @@
<string name="jf_convert_j">繁轉簡</string> <string name="jf_convert_j">繁轉簡</string>
<string name="page_mode">翻頁模式</string> <string name="page_mode">翻頁模式</string>
<string name="nb_file_sub_count">%1$d 項</string> <string name="nb_file_sub_count">%1$d 項</string>
<string name="book_count">%1$d 本</string>
<string name="nb_file_path">記憶卡:</string> <string name="nb_file_path">記憶卡:</string>
<string name="nb_file_add_shelf">加入書架</string> <string name="nb_file_add_shelf">加入書架</string>
<string name="nb_file_add_shelves">加入書架(%1$d)</string> <string name="nb_file_add_shelves">加入書架(%1$d)</string>
@@ -1222,4 +1223,6 @@
<string name="top_bar_opacity_summary">%d</string> <string name="top_bar_opacity_summary">%d</string>
<string name="bottom_bar_opacity">底欄不透明度</string> <string name="bottom_bar_opacity">底欄不透明度</string>
<string name="bottom_bar_opacity_summary">%d</string> <string name="bottom_bar_opacity_summary">%d</string>
<string name="web_file">網頁文件</string>
<string name="show_book_count">顯示分組書籍數量</string>
</resources> </resources>
+7
View File
@@ -328,6 +328,7 @@
<string name="jf_convert_j">Traditional to simplified</string> <string name="jf_convert_j">Traditional to simplified</string>
<string name="page_mode">Flipping mode</string> <string name="page_mode">Flipping mode</string>
<string name="nb_file_sub_count">%1$d items</string> <string name="nb_file_sub_count">%1$d items</string>
<string name="book_count">%1$d books</string>
<string name="nb_file_path">Storage:</string> <string name="nb_file_path">Storage:</string>
<string name="nb_file_add_shelf">Add to Bookshelf</string> <string name="nb_file_add_shelf">Add to Bookshelf</string>
<string name="nb_file_add_shelves">Add to Bookshelf(%1$d)</string> <string name="nb_file_add_shelves">Add to Bookshelf(%1$d)</string>
@@ -580,6 +581,7 @@
<string name="skip_next">Next</string> <string name="skip_next">Next</string>
<string name="music">Music</string> <string name="music">Music</string>
<string name="audio">Audio</string> <string name="audio">Audio</string>
<string name="web_file">Web file</string>
<string name="is_enable">Enable</string> <string name="is_enable">Enable</string>
<string name="enable_js">Enable js</string> <string name="enable_js">Enable js</string>
<string name="load_with_base_url">Load BaseUrl</string> <string name="load_with_base_url">Load BaseUrl</string>
@@ -1296,6 +1298,10 @@
<string name="custom_mode">Custom Mode</string> <string name="custom_mode">Custom Mode</string>
<string name="be_swiped">Pages can still be swiped</string> <string name="be_swiped">Pages can still be swiped</string>
<string name="show_bottom_nav">Show Navigation Bar</string> <string name="show_bottom_nav">Show Navigation Bar</string>
<string name="floating_bottom_bar">Floating Bottom Bar</string>
<string name="floating_bottom_bar_summary">Use a detached capsule style for the bottom navigation bar</string>
<string name="floating_bottom_bar_liquid_glass">Liquid Glass Effect</string>
<string name="floating_bottom_bar_liquid_glass_summary">Enable the liquid glass highlight for the floating bar. Requires blur and Android 13+</string>
<string name="nav_label_mode">Navigation Bar Label Mode</string> <string name="nav_label_mode">Navigation Bar Label Mode</string>
<string name="read">Read</string> <string name="read">Read</string>
<string name="privacy">Privacy</string> <string name="privacy">Privacy</string>
@@ -1376,6 +1382,7 @@
<string name="read_change_all">换源按钮默认弹出整书换源</string> <string name="read_change_all">换源按钮默认弹出整书换源</string>
<string name="read_change_all_s">反之则默认弹出单章换源</string> <string name="read_change_all_s">反之则默认弹出单章换源</string>
<string name="show_tip">显示书籍类型</string> <string name="show_tip">显示书籍类型</string>
<string name="show_book_count">显示分组书籍数量</string>
<string name="is_reading">在读</string> <string name="is_reading">在读</string>
<string name="is_unread">未读</string> <string name="is_unread">未读</string>
<string name="is_read_finished">已读</string> <string name="is_read_finished">已读</string>