[新增] 悬浮底栏和液态玻璃底栏
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user