From e38d96a6e5e654937f406ea7acfbb8bdbf763e92 Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Wed, 8 Apr 2026 02:02:50 +0800 Subject: [PATCH] =?UTF-8?q?[=E6=96=B0=E5=A2=9E]=20=E6=82=AC=E6=B5=AE?= =?UTF-8?q?=E5=BA=95=E6=A0=8F=E5=92=8C=E6=B6=B2=E6=80=81=E7=8E=BB=E7=92=83?= =?UTF-8?q?=E5=BA=95=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/legado/app/constant/PreferKey.kt | 3 + .../app/ui/animation/DampedDragAnimation.kt | 142 ++++++ .../app/ui/animation/InteractiveHighlight.kt | 109 +++++ .../app/ui/config/mainConfig/MainConfig.kt | 5 + .../app/ui/config/themeConfig/ThemeConfig.kt | 7 + .../config/themeConfig/ThemeConfigScreen.kt | 18 +- .../io/legado/app/ui/main/MainDestination.kt | 47 +- .../java/io/legado/app/ui/main/MainScreen.kt | 173 ++++++-- .../app/ui/util/DragGestureInspector.kt | 82 ++++ .../ui/widget/components/AppNavigationBar.kt | 6 +- .../ui/widget/components/FloatingBottomBar.kt | 419 ++++++++++++++++++ .../app/ui/widget/components/icon/AppIcons.kt | 65 ++- app/src/main/res/values-zh-rCN/strings.xml | 7 + app/src/main/res/values-zh-rHK/strings.xml | 3 + app/src/main/res/values-zh-rTW/strings.xml | 3 + app/src/main/res/values/strings.xml | 7 + 16 files changed, 990 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/io/legado/app/ui/animation/DampedDragAnimation.kt create mode 100644 app/src/main/java/io/legado/app/ui/animation/InteractiveHighlight.kt create mode 100644 app/src/main/java/io/legado/app/ui/util/DragGestureInspector.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/FloatingBottomBar.kt diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index daa874e6c..28df08f9d 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -207,6 +207,8 @@ object PreferKey { const val themePref = "themePref" const val bookshelfSortOrder = "bookshelfSortOrder" const val showBottomView = "showBottomView" + const val useFloatingBottomBar = "useFloatingBottomBar" + const val useFloatingBottomBarLiquidGlass = "useFloatingBottomBarLiquidGlass" const val mangaScrollMode = "mangaScrollMode" const val webtoonSidePaddingDp = "webtoonSidePaddingDp" const val mangaVolumeKeyPage = "mangaVolumeKeyPage" @@ -267,6 +269,7 @@ object PreferKey { const val defaultSourceChangeAll = "defaultSourceChangeAll" const val showTip = "showTip" + const val showBookCount = "showBookCount" const val sliderVibrator = "sliderVibrator" const val selectVibrator = "selectVibrator" const val audioCacheCleanTime = "audioCacheCleanTime" diff --git a/app/src/main/java/io/legado/app/ui/animation/DampedDragAnimation.kt b/app/src/main/java/io/legado/app/ui/animation/DampedDragAnimation.kt new file mode 100644 index 000000000..e17a536f1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/animation/DampedDragAnimation.kt @@ -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, + 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) + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/animation/InteractiveHighlight.kt b/app/src/main/java/io/legado/app/ui/animation/InteractiveHighlight.kt new file mode 100644 index 000000000..aea50ca1e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/animation/InteractiveHighlight.kt @@ -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) } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/config/mainConfig/MainConfig.kt b/app/src/main/java/io/legado/app/ui/config/mainConfig/MainConfig.kt index 6cb709d02..d035bde58 100644 --- a/app/src/main/java/io/legado/app/ui/config/mainConfig/MainConfig.kt +++ b/app/src/main/java/io/legado/app/ui/config/mainConfig/MainConfig.kt @@ -7,6 +7,11 @@ object MainConfig { var showDiscovery by prefDelegate(PreferKey.showDiscovery, true) var showRSS by prefDelegate(PreferKey.showRss, 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 tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto") var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto") diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt index 4bcec4533..b34636be3 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt +++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt @@ -72,6 +72,13 @@ object ThemeConfig { 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 labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto") diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt index 5b03d8376..89f79c023 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt +++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt @@ -35,7 +35,6 @@ import androidx.compose.material.icons.filled.BrightnessMedium import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.LightMode -import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -43,10 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.ToggleButton import androidx.compose.material3.ToggleButtonDefaults import androidx.compose.runtime.Composable @@ -375,6 +371,20 @@ fun ThemeConfigScreen( checked = ThemeConfig.showBottomView, 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( title = stringResource(R.string.tabletInterface), selectedValue = ThemeConfig.tabletInterface, diff --git a/app/src/main/java/io/legado/app/ui/main/MainDestination.kt b/app/src/main/java/io/legado/app/ui/main/MainDestination.kt index 7db8fd46c..27fa25a46 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainDestination.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainDestination.kt @@ -1,68 +1,33 @@ package io.legado.app.ui.main 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 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( val route: String, - @StringRes val labelId: Int, - val m3Icon: ImageVector, - val m3SelectedIcon: ImageVector, - val miuixIcon: ImageVector, - val miuixSelectedIcon: ImageVector + @StringRes val labelId: Int ) { object Bookshelf : MainDestination( route = "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 填充图标 + labelId = R.string.bookshelf ) object Explore : MainDestination( route = "explore", - labelId = R.string.discovery, - m3Icon = Icons.Outlined.Explore, - m3SelectedIcon = Icons.Default.Explore, - miuixIcon = MiuixIcons.Regular.Album, - miuixSelectedIcon = MiuixIcons.Heavy.Album + labelId = R.string.discovery ) object Rss : MainDestination( route = "rss", - labelId = R.string.rss, - m3Icon = Icons.Outlined.RssFeed, - m3SelectedIcon = Icons.Default.RssFeed, - miuixIcon = MiuixIcons.Regular.Favorites, - miuixSelectedIcon = MiuixIcons.Heavy.Favorites + labelId = R.string.rss ) object My : MainDestination( route = "my", - labelId = R.string.my, - m3Icon = Icons.Outlined.Person, - m3SelectedIcon = Icons.Default.Person, - miuixIcon = MiuixIcons.Regular.Settings, - miuixSelectedIcon = MiuixIcons.Heavy.Settings + labelId = R.string.my ) companion object { val mainDestinations = listOf(Bookshelf, Explore, Rss, My) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt index 1b19e510e..fa38388af 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt @@ -1,14 +1,21 @@ package io.legado.app.ui.main +import android.os.Build import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.HorizontalPager @@ -39,15 +46,18 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp 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.hazeSource import io.legado.app.R import io.legado.app.ui.book.info.BookInfoActivity 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.my.MyScreen 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.responsiveHazeSource import io.legado.app.ui.widget.components.AppNavigationBar import io.legado.app.ui.widget.components.AppNavigationBarItem 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.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.RoundDropdownMenuItem import io.legado.app.ui.widget.components.text.AppText @@ -88,6 +104,11 @@ fun MainScreen( val bookshelfUiState by bookshelfViewModel.uiState.collectAsState() val hazeState = remember { HazeState() } + val floatingBarSurfaceColor = MaterialTheme.colorScheme.surface + val floatingBarBackdrop = rememberLayerBackdrop { + drawRect(floatingBarSurfaceColor) + drawContent() + } val destinations = remember(MainConfig.showDiscovery, MainConfig.showRSS) { MainDestination.mainDestinations.filter { when (it) { @@ -103,6 +124,15 @@ fun MainScreen( if (index != -1) index else 0 } 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( initialValue = if (MainConfig.navExtended) @@ -230,51 +260,116 @@ fun MainScreen( } AppScaffold( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .responsiveHazeSource(hazeState), bottomBar = { if (!useRail && MainConfig.showBottomView) { - val labelVisibilityMode = MainConfig.labelVisibilityMode - val isUnlabeled = labelVisibilityMode == "unlabeled" - - AppNavigationBar( - modifier = Modifier - .regularHazeEffect(state = hazeState) - .height(if (isUnlabeled) 64.dp else 80.dp) - ) { - val alwaysShowLabel = when (labelVisibilityMode) { - "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 + if (useFloatingBottomBar) { + Box(modifier = Modifier.fillMaxWidth()) { + FloatingBottomBar( + modifier = Modifier + .align(Alignment.BottomCenter) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = {} ) + .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( - noBlurColor = MaterialTheme.colorScheme.secondaryContainer, - blurAlpha = GlassDefaults.ThickBlurAlpha - ), - m3ShowLabel = !isUnlabeled, - m3AlwaysShowLabel = alwaysShowLabel - ) + backdrop = floatingBarBackdrop, + tabsCount = destinations.size, + isBlurEnabled = useLiquidGlass + ) { + destinations.forEachIndexed { index, destination -> + 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) ) { _ -> - Box(modifier = Modifier.hazeSource(hazeState)) { + Box( + modifier = Modifier + .then( + if (useFloatingBottomBar) { + Modifier.layerBackdrop(floatingBarBackdrop) + } else { + Modifier + } + ) + ) { HorizontalPager( state = pagerState, modifier = Modifier.fillMaxSize(), @@ -319,14 +414,14 @@ private fun NavigationIcon( upBooksCount: Int, modifier: Modifier = Modifier ) { - val icon = if (selected) destination.m3SelectedIcon else destination.m3Icon + val icon = AppIcons.mainDestination(destination, selected) Box(modifier = modifier) { if (destination == MainDestination.Bookshelf && upBooksCount > 0) { BadgedBox(badge = { Badge { Text(upBooksCount.toString()) } }) { - Icon(icon, contentDescription = null) + AppIcon(icon, contentDescription = null) } } else { - Icon(icon, contentDescription = null) + AppIcon(icon, contentDescription = null) } } } diff --git a/app/src/main/java/io/legado/app/ui/util/DragGestureInspector.kt b/app/src/main/java/io/legado/app/ui/util/DragGestureInspector.kt new file mode 100644 index 000000000..a91a521a8 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/util/DragGestureInspector.kt @@ -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 + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/AppNavigationBar.kt b/app/src/main/java/io/legado/app/ui/widget/components/AppNavigationBar.kt index f22b6a59b..f20eb06a0 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/AppNavigationBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/AppNavigationBar.kt @@ -61,7 +61,7 @@ fun RowScope.AppNavigationBarItem( onClick: () -> Unit, modifier: Modifier = Modifier, labelString: String, - miuixIcon: ImageVector, + iconVector: ImageVector, m3Icon: @Composable () -> Unit, m3IndicatorColor: Color, m3ShowLabel: Boolean, @@ -73,7 +73,7 @@ fun RowScope.AppNavigationBarItem( MiuixNavigationBarItem( selected = selected, onClick = onClick, - icon = miuixIcon, + icon = iconVector, label = labelString, modifier = modifier ) @@ -90,4 +90,4 @@ fun RowScope.AppNavigationBarItem( alwaysShowLabel = m3AlwaysShowLabel ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/FloatingBottomBar.kt b/app/src/main/java/io/legado/app/ui/widget/components/FloatingBottomBar.kt new file mode 100644 index 000000000..7d6917c71 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/FloatingBottomBar.kt @@ -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() }) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt b/app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt index df274d7fd..699410b35 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt @@ -2,22 +2,32 @@ package io.legado.app.ui.widget.components.icon import androidx.compose.material.icons.Icons 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.Explore +import androidx.compose.material.icons.filled.Person 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.outlined.Explore +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.RssFeed import androidx.compose.runtime.Composable 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.ThemeResolver import top.yukonga.miuix.kmp.icon.MiuixIcons 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.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.Settings -/** - * 应用全局图标映射层 - */ object AppIcons { private val isMiuix: Boolean @@ -26,29 +36,46 @@ object AppIcons { val Search: ImageVector @Composable - get() = if (isMiuix) { - MiuixIcons.Basic.Search - } else { - Icons.Default.Search - } + get() = if (isMiuix) MiuixIcons.Basic.Search else Icons.Default.Search val Close: ImageVector @Composable - get() = if (isMiuix) { - MiuixIcons.Close - } else { - Icons.Default.Clear - } + get() = if (isMiuix) MiuixIcons.Close else Icons.Default.Clear val Back: ImageVector @Composable - get() = if (isMiuix) { - MiuixIcons.Back - } else { - Icons.AutoMirrored.Filled.ArrowBack - } + get() = if (isMiuix) MiuixIcons.Back else Icons.AutoMirrored.Filled.ArrowBack val Replay: ImageVector @Composable get() = if (isMiuix) MiuixIcons.Refresh ?: Icons.Default.Replay else Icons.Default.Replay -} \ No newline at end of file + + @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 + } + } + } +} diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a4f7ff643..0cb955f8a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -325,6 +325,7 @@ 繁转简 翻页模式 %1$d 项 + %1$d 本 存储卡: 加入书架 加入书架(%1$d) @@ -577,6 +578,7 @@ 下一个 音乐 音频 + 网页文件 启用 启用 JavaScript 加载 BaseUrl @@ -1293,6 +1295,10 @@ 自定义模式 仍可滑动切换页面 显示导航栏 + 悬浮底栏 + 底部导航栏使用悬浮胶囊样式 + 液态玻璃效果 + 为悬浮底栏启用液态玻璃高光效果,需要 Android 13 及以上系统 导航栏标签显示方式 阅读 隐私 @@ -1372,6 +1378,7 @@ 换源按钮默认弹出整书换源 反之则默认弹出单章换源 显示书籍类型 + 显示分组书籍数量 在读 未读 已读 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 4c58e6c5c..36151103c 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -302,6 +302,7 @@ 繁轉簡 翻頁模式 %1$d 項 + %1$d 本 存儲咭: 加入書架 加入書架 (%1$d) @@ -1220,4 +1221,6 @@ %d 底欄不透明度 %d + 網頁文件 + 顯示分組書籍數量 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index d88fba834..44cdbf65f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -304,6 +304,7 @@ 繁轉簡 翻頁模式 %1$d 項 + %1$d 本 記憶卡: 加入書架 加入書架(%1$d) @@ -1222,4 +1223,6 @@ %d 底欄不透明度 %d + 網頁文件 + 顯示分組書籍數量 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5be095fe9..762183443 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -328,6 +328,7 @@ Traditional to simplified Flipping mode %1$d items + %1$d books Storage: Add to Bookshelf Add to Bookshelf(%1$d) @@ -580,6 +581,7 @@ Next Music Audio + Web file Enable Enable js Load BaseUrl @@ -1296,6 +1298,10 @@ Custom Mode Pages can still be swiped Show Navigation Bar + Floating Bottom Bar + Use a detached capsule style for the bottom navigation bar + Liquid Glass Effect + Enable the liquid glass highlight for the floating bar. Requires blur and Android 13+ Navigation Bar Label Mode Read Privacy @@ -1376,6 +1382,7 @@ 换源按钮默认弹出整书换源 反之则默认弹出单章换源 显示书籍类型 + 显示分组书籍数量 在读 未读 已读