From cf0892e85fecee60d20fab55f37cb623a4104efc Mon Sep 17 00:00:00 2001 From: zimya <57585727+zimya@users.noreply.github.com> Date: Sat, 30 May 2026 14:44:24 +0800 Subject: [PATCH 1/4] fix: jump to user defined tab when launch. --- .../bangumi/features/splash/SplashScreen.kt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/features/splash/src/commonMain/kotlin/com/xiaoyv/bangumi/features/splash/SplashScreen.kt b/features/splash/src/commonMain/kotlin/com/xiaoyv/bangumi/features/splash/SplashScreen.kt index a0622e6b2..f219394d9 100644 --- a/features/splash/src/commonMain/kotlin/com/xiaoyv/bangumi/features/splash/SplashScreen.kt +++ b/features/splash/src/commonMain/kotlin/com/xiaoyv/bangumi/features/splash/SplashScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -48,19 +47,9 @@ fun SplashRoute(onNavScreen: (Screen) -> Unit) { @Composable fun SplashScreen(onNavScreen: (Screen) -> Unit) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Button(onClick = { onNavScreen(Screen.Main) }) { - Text(text = "Home") - } + LaunchedEffect(Unit) { + onNavScreen(Screen.Main) } -// Navigation() - -// LaunchedEffect(Unit) { -// onNavScreen(Screen.Main) -// } } @Composable From dfb6fd674f89fead063f4e40663e77fd19838c39 Mon Sep 17 00:00:00 2001 From: zimya <57585727+zimya@users.noreply.github.com> Date: Sat, 30 May 2026 15:56:41 +0800 Subject: [PATCH 2/4] fix: navigation error, No virtual method removeLast(). --- gradle/libs.versions.toml | 2 +- .../bangumi/shared/ui/component/navigation/Navigator.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5e6a75f1..eda902998 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ androidx-activityCompose = "1.12.2" immersionbar = "3.2.2" androidx-lifecycle = "2.10.0-alpha08" compose-adaptive = "1.3.0-alpha04" -navigation3 = "1.1.0-beta01" +navigation3 = "1.1.1" orbit-mvi = "11.0.0" cryptohash = "1.0.2" cryptorandom = "0.6.0" diff --git a/shared/ui/src/commonMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/navigation/Navigator.kt b/shared/ui/src/commonMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/navigation/Navigator.kt index 7f5640e13..129af631a 100644 --- a/shared/ui/src/commonMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/navigation/Navigator.kt +++ b/shared/ui/src/commonMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/navigation/Navigator.kt @@ -130,7 +130,7 @@ val NavBackStack.current fun NavBackStack.goBack(): Boolean { if (size <= 1) return false - removeLast() + removeLastOrNull() return true } @@ -144,7 +144,7 @@ fun NavBackStack.navigate( if (index != -1) { val cutIndex = if (popUpToInclusive) index else index + 1 while (size > cutIndex) { - removeLast() + removeLastOrNull() } } } From 60f0ba5fbc2525640bbc98e6892e79f17cfe657a Mon Sep 17 00:00:00 2001 From: zimya <57585727+zimya@users.noreply.github.com> Date: Sat, 30 May 2026 18:11:26 +0800 Subject: [PATCH 3/4] fix: text color adapted to the blur image background color. --- .../groups/detail/GroupsDetailScreen.kt | 24 +++++-- .../index/detail/IndexDetailScreen.kt | 22 +++++-- .../main/tab/profile/ProfileScreen.kt | 25 +++++-- .../DefaultNavigationSuiteScaffoldOverride.kt | 4 +- .../features/mono/detail/MonoDetailScreen.kt | 24 +++++-- .../subject/detail/SubjectDetailScreen.kt | 24 +++++-- .../bangumi/features/user/UserScreen.kt | 20 ++++-- .../image/ImageColorState.android.kt | 59 +++++++++++++++++ .../ui/component/image/ImageColorState.kt | 66 +++++++++++++++++++ .../ui/component/image/ImageColorState.ios.kt | 44 +++++++++++++ .../ui/component/image/ImageColorState.jvm.kt | 44 +++++++++++++ 11 files changed, 326 insertions(+), 30 deletions(-) create mode 100644 shared/ui/src/androidMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.android.kt create mode 100644 shared/ui/src/commonMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.kt create mode 100644 shared/ui/src/iosMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.ios.kt create mode 100644 shared/ui/src/jvmMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.jvm.kt diff --git a/features/groups/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/groups/detail/GroupsDetailScreen.kt b/features/groups/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/groups/detail/GroupsDetailScreen.kt index 1cb3b31e4..3dc14d590 100644 --- a/features/groups/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/groups/detail/GroupsDetailScreen.kt +++ b/features/groups/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/groups/detail/GroupsDetailScreen.kt @@ -55,7 +55,9 @@ import com.xiaoyv.bangumi.shared.ui.component.action.LocalActionHandler import com.xiaoyv.bangumi.shared.ui.component.bar.BgmTopAppBar import com.xiaoyv.bangumi.shared.ui.component.chip.DropMenuActionButton import com.xiaoyv.bangumi.shared.ui.component.image.BlurImage +import com.xiaoyv.bangumi.shared.ui.component.image.ImageColorState import com.xiaoyv.bangumi.shared.ui.component.image.StateImage +import com.xiaoyv.bangumi.shared.ui.component.image.rememberImageColorState import com.xiaoyv.bangumi.shared.ui.component.layout.BgmCollapsingScaffold import com.xiaoyv.bangumi.shared.ui.component.layout.LocalCollapsingPullRefresh import com.xiaoyv.bangumi.shared.ui.component.layout.state.StateLayout @@ -102,14 +104,23 @@ private fun GroupsDetailScreen( onUiEvent: (GroupsDetailEvent.UI) -> Unit, onActionEvent: (GroupsDetailEvent.Action) -> Unit, ) { + val imageColorState = rememberImageColorState() + BgmCollapsingScaffold( modifier = Modifier.fillMaxSize(), topBar = { + val iconColor = androidx.compose.ui.graphics.lerp( + imageColorState.contentColor, + MaterialTheme.colorScheme.onSurface, + it + ) BgmTopAppBar( title = baseState.content { group.title }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface.copy(alpha = it), titleContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = it), + navigationIconContentColor = iconColor, + actionIconContentColor = iconColor ), actions = { baseState.content { @@ -139,6 +150,7 @@ private fun GroupsDetailScreen( GroupsDetailScreenHeader( state = this, padding = it, + imageColorState = imageColorState, onUiEvent = onUiEvent, onActionEvent = onActionEvent ) @@ -162,6 +174,7 @@ private fun GroupsDetailScreen( private fun GroupsDetailScreenHeader( state: GroupsDetailState, padding: PaddingValues, + imageColorState: ImageColorState, onUiEvent: (GroupsDetailEvent.UI) -> Unit, onActionEvent: (GroupsDetailEvent.Action) -> Unit, ) { @@ -170,7 +183,8 @@ private fun GroupsDetailScreenHeader( modifier = Modifier.matchParentSize(), model = state.group.images.displayGridImage, contentDescription = state.group.name, - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + onState = imageColorState.onImageState ) Box( @@ -190,8 +204,8 @@ private fun GroupsDetailScreenHeader( .padding(vertical = LayoutPaddingHalf), colors = ListItemDefaults.colors( containerColor = Color.Transparent, - headlineColor = MaterialTheme.colorScheme.onPrimary, - supportingColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f) + headlineColor = imageColorState.contentColor, + supportingColor = imageColorState.contentColorSecondary ), leadingContent = { StateImage( @@ -227,9 +241,9 @@ private fun GroupsDetailScreenHeader( OutlinedButton( shape = MaterialTheme.shapes.small, colors = ButtonDefaults.outlinedButtonColors( - contentColor = if (state.group.isJoined) MaterialTheme.colorScheme.onPrimary else StarColor, + contentColor = if (state.group.isJoined) imageColorState.contentColor else StarColor, ), - border = BorderStroke(1.dp, if (state.group.isJoined) MaterialTheme.colorScheme.onPrimary else StarColor), + border = BorderStroke(1.dp, if (state.group.isJoined) imageColorState.contentColor else StarColor), onClick = { onActionEvent(GroupsDetailEvent.Action.OnToggleJoinGroup) } ) { Text( diff --git a/features/index/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/index/detail/IndexDetailScreen.kt b/features/index/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/index/detail/IndexDetailScreen.kt index 7de8f5e15..b3d25deaf 100644 --- a/features/index/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/index/detail/IndexDetailScreen.kt +++ b/features/index/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/index/detail/IndexDetailScreen.kt @@ -50,7 +50,9 @@ import com.xiaoyv.bangumi.shared.ui.component.action.LocalActionHandler import com.xiaoyv.bangumi.shared.ui.component.bar.BgmTopAppBar import com.xiaoyv.bangumi.shared.ui.component.chip.DropMenuActionButton import com.xiaoyv.bangumi.shared.ui.component.image.BlurImage +import com.xiaoyv.bangumi.shared.ui.component.image.ImageColorState import com.xiaoyv.bangumi.shared.ui.component.image.StateImage +import com.xiaoyv.bangumi.shared.ui.component.image.rememberImageColorState import com.xiaoyv.bangumi.shared.ui.component.layout.BgmCollapsingScaffold import com.xiaoyv.bangumi.shared.ui.component.layout.LocalCollapsingPullRefresh import com.xiaoyv.bangumi.shared.ui.component.layout.state.StateLayout @@ -98,14 +100,23 @@ private fun IndexDetailScreen( onUiEvent: (IndexDetailEvent.UI) -> Unit, onActionEvent: (IndexDetailEvent.Action) -> Unit, ) { + val imageColorState = rememberImageColorState() + BgmCollapsingScaffold( modifier = Modifier.fillMaxSize(), topBar = { + val iconColor = androidx.compose.ui.graphics.lerp( + imageColorState.contentColor, + MaterialTheme.colorScheme.onSurface, + it + ) BgmTopAppBar( title = baseState.content { index.title }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface.copy(alpha = it), titleContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = it), + navigationIconContentColor = iconColor, + actionIconContentColor = iconColor ), actions = { baseState.content { @@ -142,6 +153,7 @@ private fun IndexDetailScreen( IndexDetailScreenHeader( state = this, padding = it, + imageColorState = imageColorState, onUiEvent = onUiEvent, onActionEvent = onActionEvent ) @@ -164,6 +176,7 @@ private fun IndexDetailScreen( private fun IndexDetailScreenHeader( state: IndexDetailState, padding: PaddingValues, + imageColorState: ImageColorState, onUiEvent: (IndexDetailEvent.UI) -> Unit, onActionEvent: (IndexDetailEvent.Action) -> Unit, ) { @@ -172,7 +185,8 @@ private fun IndexDetailScreenHeader( modifier = Modifier.matchParentSize(), model = state.index.creator.avatar.displayGridImage, contentDescription = state.index.title, - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + onState = imageColorState.onImageState ) Box( @@ -192,8 +206,8 @@ private fun IndexDetailScreenHeader( .padding(vertical = LayoutPaddingHalf), colors = ListItemDefaults.colors( containerColor = Color.Transparent, - headlineColor = MaterialTheme.colorScheme.onPrimary, - supportingColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f) + headlineColor = imageColorState.contentColor, + supportingColor = imageColorState.contentColorSecondary ), leadingContent = { StateImage( @@ -225,7 +239,7 @@ private fun IndexDetailScreenHeader( ) Text( text = buildAnnotatedString { - withStyle(SpanStyle(fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onPrimary)) { + withStyle(SpanStyle(fontWeight = FontWeight.Medium, color = imageColorState.contentColor)) { append(state.index.creator.nickname) } append(" 创建 ") diff --git a/features/main-tab/profile/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/tab/profile/ProfileScreen.kt b/features/main-tab/profile/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/tab/profile/ProfileScreen.kt index 250af1c4e..7df13abb8 100644 --- a/features/main-tab/profile/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/tab/profile/ProfileScreen.kt +++ b/features/main-tab/profile/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/tab/profile/ProfileScreen.kt @@ -60,7 +60,9 @@ import com.xiaoyv.bangumi.shared.data.manager.shared.currentUser import com.xiaoyv.bangumi.shared.ui.component.bar.BgmTopAppBar import com.xiaoyv.bangumi.shared.ui.component.chip.DropMenuActionButton import com.xiaoyv.bangumi.shared.ui.component.image.BlurImage +import com.xiaoyv.bangumi.shared.ui.component.image.ImageColorState import com.xiaoyv.bangumi.shared.ui.component.image.StateImage +import com.xiaoyv.bangumi.shared.ui.component.image.rememberImageColorState import com.xiaoyv.bangumi.shared.ui.component.layout.BgmCollapsingScaffold import com.xiaoyv.bangumi.shared.ui.component.layout.BgmRequireLoginLayout import com.xiaoyv.bangumi.shared.ui.component.layout.LocalCollapsingPullRefresh @@ -109,14 +111,25 @@ private fun ProfileScreen( val scrollState = rememberScrollState() val user = currentUser() val sharedState = LocalSharedState.current + val imageColorState = rememberImageColorState() BgmCollapsingScaffold( modifier = Modifier.fillMaxSize(), state = scrollState, topBar = { + val iconColor = androidx.compose.ui.graphics.lerp( + imageColorState.contentColor, + MaterialTheme.colorScheme.onSurface, + it + ) BgmTopAppBar( title = currentUser().nickname, - colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.surface.copy(alpha = it)), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface.copy(alpha = it), + titleContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = it), + navigationIconContentColor = iconColor, + actionIconContentColor = iconColor + ), navigationIcon = { DropMenuActionButton( imageVector = BgmIcons.Menu, @@ -177,7 +190,7 @@ private fun ProfileScreen( }, collapse = { baseState.content { - ProfileScreenHeader(state = this, it, onUiEvent, onActionEvent) + ProfileScreenHeader(state = this, it, imageColorState, onUiEvent, onActionEvent) } } ) { @@ -199,6 +212,7 @@ private fun ProfileScreen( private fun ProfileScreenHeader( state: ProfileState, padding: PaddingValues, + imageColorState: ImageColorState, onUiEvent: (ProfileEvent.UI) -> Unit, onActionEvent: (ProfileEvent.Action) -> Unit, ) { @@ -215,7 +229,8 @@ private fun ProfileScreenHeader( modifier = Modifier.fillMaxSize(), model = avatar, contentDescription = stringResource(Res.string.global_image), - androidSampling = if (avatar.isBlank()) 20f else 5f + androidSampling = if (avatar.isBlank()) 20f else 5f, + onState = imageColorState.onImageState ) Column( @@ -247,13 +262,13 @@ private fun ProfileScreenHeader( modifier = Modifier.padding(top = LayoutPaddingHalf), text = currentUser.nickname.ifBlank { stringResource(Res.string.login_first_tip) }, style = MaterialTheme.typography.titleLarge.copy( - color = MaterialTheme.colorScheme.surface + color = imageColorState.contentColor ) ) Text( text = "@" + currentUser.username.ifBlank { "bangumi" }, style = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.surface + color = imageColorState.contentColor ) ) } diff --git a/features/main/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/DefaultNavigationSuiteScaffoldOverride.kt b/features/main/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/DefaultNavigationSuiteScaffoldOverride.kt index a447f4ea3..68ac92180 100644 --- a/features/main/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/DefaultNavigationSuiteScaffoldOverride.kt +++ b/features/main/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/DefaultNavigationSuiteScaffoldOverride.kt @@ -67,7 +67,7 @@ object DefaultNavigationSuiteScaffoldOverride : NavigationSuiteScaffoldOverride NavigationItemIcon(icon = it.icon, badge = it.badge) ProvideContentColorTextStyle( - contentColor = contentColor, + contentColor = MaterialTheme.colorScheme.primary, textStyle = MaterialTheme.typography.bodySmall, content = { it.label?.invoke() @@ -87,7 +87,7 @@ internal fun ProvideContentColorTextStyle( textStyle: TextStyle, content: @Composable () -> Unit, ) { - val mergedStyle = LocalTextStyle.current.merge(textStyle) + val mergedStyle = LocalTextStyle.current.merge(textStyle).copy(color = contentColor) CompositionLocalProvider( LocalContentColor provides contentColor, LocalTextStyle provides mergedStyle, diff --git a/features/mono/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/mono/detail/MonoDetailScreen.kt b/features/mono/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/mono/detail/MonoDetailScreen.kt index 88568d8fb..80ede55bc 100644 --- a/features/mono/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/mono/detail/MonoDetailScreen.kt +++ b/features/mono/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/mono/detail/MonoDetailScreen.kt @@ -63,7 +63,9 @@ import com.xiaoyv.bangumi.shared.ui.component.bar.BgmTopAppBar import com.xiaoyv.bangumi.shared.ui.component.chip.DropMenuActionButton import com.xiaoyv.bangumi.shared.ui.component.dialog.sheet.rememberSheetDialogState import com.xiaoyv.bangumi.shared.ui.component.image.BlurImage +import com.xiaoyv.bangumi.shared.ui.component.image.ImageColorState import com.xiaoyv.bangumi.shared.ui.component.image.StateImage +import com.xiaoyv.bangumi.shared.ui.component.image.rememberImageColorState import com.xiaoyv.bangumi.shared.ui.component.layout.BgmCollapsingScaffold import com.xiaoyv.bangumi.shared.ui.component.layout.LocalCollapsingPullRefresh import com.xiaoyv.bangumi.shared.ui.component.layout.state.StateLayout @@ -121,15 +123,24 @@ private fun MonoDetailScreen( onUiEvent: (MonoDetailEvent.UI) -> Unit, onActionEvent: (MonoDetailEvent.Action) -> Unit, ) { + val imageColorState = rememberImageColorState() + BgmCollapsingScaffold( modifier = Modifier.fillMaxSize(), topBar = { + val iconColor = androidx.compose.ui.graphics.lerp( + imageColorState.contentColor, + MaterialTheme.colorScheme.onSurface, + it + ) BgmTopAppBar( modifier = Modifier.fillMaxWidth(), title = baseState.payload?.mono?.displayName, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface.copy(alpha = it), - titleContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = it) + titleContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = it), + navigationIconContentColor = iconColor, + actionIconContentColor = iconColor ), actions = { baseState.content { @@ -197,6 +208,7 @@ private fun MonoDetailScreen( .background(MaterialTheme.colorScheme.secondaryContainer), insets = it, state = this, + imageColorState = imageColorState, onUiEvent = onUiEvent, onActionEvent = onActionEvent ) @@ -227,6 +239,7 @@ private fun MonoDetailScreenHeader( modifier: Modifier, insets: PaddingValues, state: MonoDetailState, + imageColorState: ImageColorState, onUiEvent: (MonoDetailEvent.UI) -> Unit, onActionEvent: (MonoDetailEvent.Action) -> Unit, ) { @@ -235,7 +248,8 @@ private fun MonoDetailScreenHeader( modifier = Modifier.matchParentSize(), contentScale = ContentScale.FillBounds, model = state.mono.images.displayGridImage, - contentDescription = state.mono.displayName + contentDescription = state.mono.displayName, + onState = imageColorState.onImageState ) Row( @@ -275,7 +289,7 @@ private fun MonoDetailScreenHeader( Text( text = state.mono.displayName, style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onPrimary, + color = imageColorState.contentColor, maxLines = 2, overflow = TextOverflow.Ellipsis ) @@ -284,7 +298,7 @@ private fun MonoDetailScreenHeader( modifier = Modifier.basicMarquee(), text = state.mono.name, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onPrimary, + color = imageColorState.contentColor, maxLines = 1, ) @@ -292,7 +306,7 @@ private fun MonoDetailScreenHeader( Text( text = "", style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f), + color = imageColorState.contentColorSecondary, maxLines = 1, overflow = TextOverflow.MiddleEllipsis, ) diff --git a/features/subject/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/subject/detail/SubjectDetailScreen.kt b/features/subject/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/subject/detail/SubjectDetailScreen.kt index 80ba70ada..7a16ec444 100644 --- a/features/subject/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/subject/detail/SubjectDetailScreen.kt +++ b/features/subject/detail/src/commonMain/kotlin/com/xiaoyv/bangumi/features/subject/detail/SubjectDetailScreen.kt @@ -71,7 +71,9 @@ import com.xiaoyv.bangumi.shared.ui.component.bar.BgmTopAppBar import com.xiaoyv.bangumi.shared.ui.component.chip.DropMenuActionButton import com.xiaoyv.bangumi.shared.ui.component.dialog.sheet.rememberSheetDialogState import com.xiaoyv.bangumi.shared.ui.component.image.BlurImage +import com.xiaoyv.bangumi.shared.ui.component.image.ImageColorState import com.xiaoyv.bangumi.shared.ui.component.image.StateImage +import com.xiaoyv.bangumi.shared.ui.component.image.rememberImageColorState import com.xiaoyv.bangumi.shared.ui.component.layout.BgmCollapsingScaffold import com.xiaoyv.bangumi.shared.ui.component.layout.LocalCollapsingPullRefresh import com.xiaoyv.bangumi.shared.ui.component.layout.state.StateLayout @@ -127,15 +129,24 @@ private fun SubjectDetailScreen( onUiEvent: (SubjectDetailEvent.UI) -> Unit, onActionEvent: (SubjectDetailEvent.Action) -> Unit, ) { + val imageColorState = rememberImageColorState() + BgmCollapsingScaffold( modifier = Modifier.fillMaxSize(), topBar = { + val iconColor = androidx.compose.ui.graphics.lerp( + imageColorState.contentColor, + MaterialTheme.colorScheme.onSurface, + it + ) BgmTopAppBar( modifier = Modifier.fillMaxWidth(), title = baseState.payload?.subject?.displayName.orEmpty(), colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface.copy(alpha = it), - titleContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = it) + titleContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = it), + navigationIconContentColor = iconColor, + actionIconContentColor = iconColor ), actions = { baseState.content { @@ -215,6 +226,7 @@ private fun SubjectDetailScreen( .background(MaterialTheme.colorScheme.secondaryContainer), insets = it, state = this, + imageColorState = imageColorState, onUiEvent = onUiEvent, onActionEvent = onActionEvent ) @@ -244,6 +256,7 @@ private fun SubjectDetailScreenHeader( modifier: Modifier, insets: PaddingValues, state: SubjectDetailState, + imageColorState: ImageColorState, onUiEvent: (SubjectDetailEvent.UI) -> Unit, onActionEvent: (SubjectDetailEvent.Action) -> Unit, ) { @@ -254,7 +267,8 @@ private fun SubjectDetailScreenHeader( contentScale = ContentScale.FillBounds, filterQuality = FilterQuality.High, contentDescription = state.subject.displayName, - androidSampling = 2f + androidSampling = 2f, + onState = imageColorState.onImageState ) Row( @@ -337,7 +351,7 @@ private fun SubjectDetailScreenHeader( Text( text = state.subject.displayName, style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onPrimary, + color = imageColorState.contentColor, maxLines = 2, overflow = TextOverflow.Ellipsis ) @@ -347,14 +361,14 @@ private fun SubjectDetailScreenHeader( modifier = Modifier.basicMarquee(), text = state.subject.name, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onPrimary, + color = imageColorState.contentColor, maxLines = 1, ) // 日期 Text( text = state.subject.rememberDisplayDateAndType(), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f), + color = imageColorState.contentColorSecondary, maxLines = 1, overflow = TextOverflow.MiddleEllipsis, ) diff --git a/features/user/src/commonMain/kotlin/com/xiaoyv/bangumi/features/user/UserScreen.kt b/features/user/src/commonMain/kotlin/com/xiaoyv/bangumi/features/user/UserScreen.kt index 04c26750d..f6dfb89cb 100644 --- a/features/user/src/commonMain/kotlin/com/xiaoyv/bangumi/features/user/UserScreen.kt +++ b/features/user/src/commonMain/kotlin/com/xiaoyv/bangumi/features/user/UserScreen.kt @@ -41,7 +41,9 @@ import com.xiaoyv.bangumi.shared.ui.component.action.LocalActionHandler import com.xiaoyv.bangumi.shared.ui.component.bar.BgmTopAppBar import com.xiaoyv.bangumi.shared.ui.component.chip.DropMenuActionButton import com.xiaoyv.bangumi.shared.ui.component.image.BlurImage +import com.xiaoyv.bangumi.shared.ui.component.image.ImageColorState import com.xiaoyv.bangumi.shared.ui.component.image.StateImage +import com.xiaoyv.bangumi.shared.ui.component.image.rememberImageColorState import com.xiaoyv.bangumi.shared.ui.component.layout.BgmCollapsingScaffold import com.xiaoyv.bangumi.shared.ui.component.layout.LocalCollapsingPullRefresh import com.xiaoyv.bangumi.shared.ui.component.layout.state.StateLayout @@ -85,16 +87,24 @@ private fun UserScreen( onActionEvent: (UserEvent.Action) -> Unit, ) { val scrollState = rememberScrollState() + val imageColorState = rememberImageColorState() BgmCollapsingScaffold( modifier = Modifier.fillMaxSize(), state = scrollState, topBar = { + val iconColor = androidx.compose.ui.graphics.lerp( + imageColorState.contentColor, + MaterialTheme.colorScheme.onSurface, + it + ) BgmTopAppBar( title = baseState.payload?.user?.nickname, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface.copy(alpha = it), - titleContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = it) + titleContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = it), + navigationIconContentColor = iconColor, + actionIconContentColor = iconColor ), actions = { baseState.content { @@ -121,7 +131,7 @@ private fun UserScreen( }, collapse = { baseState.content { - UserScreenHeader(state = this, it, onUiEvent, onActionEvent) + UserScreenHeader(state = this, it, imageColorState, onUiEvent, onActionEvent) } } ) { @@ -141,6 +151,7 @@ private fun UserScreen( private fun UserScreenHeader( state: UserState, padding: PaddingValues, + imageColorState: ImageColorState, onUiEvent: (UserEvent.UI) -> Unit, onActionEvent: (UserEvent.Action) -> Unit, ) { @@ -153,7 +164,8 @@ private fun UserScreenHeader( BlurImage( modifier = Modifier.fillMaxSize(), model = state.user.avatar.displayGridImage, - contentDescription = stringResource(Res.string.global_image) + contentDescription = stringResource(Res.string.global_image), + onState = imageColorState.onImageState ) Column( @@ -179,7 +191,7 @@ private fun UserScreenHeader( modifier = Modifier.padding(top = LayoutPaddingHalf), text = state.user.nickname + "@" + state.user.username, style = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.surface + color = imageColorState.contentColor ) ) } diff --git a/shared/ui/src/androidMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.android.kt b/shared/ui/src/androidMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.android.kt new file mode 100644 index 000000000..4c8ef5ad2 --- /dev/null +++ b/shared/ui/src/androidMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.android.kt @@ -0,0 +1,59 @@ +package com.xiaoyv.bangumi.shared.ui.component.image + +import android.graphics.Bitmap +import coil3.BitmapImage +import coil3.Image + +/** + * Android 平台实现:从 Coil3 Image 中提取平均亮度 + * + * 通过采样像素计算平均亮度,避免遍历所有像素导致性能问题。 + * 注意:Coil 默认使用 HARDWARE Bitmap,不支持 getPixel(),需要先 copy 为软件 Bitmap。 + */ +actual fun computeAverageLuminance(image: Image): Float { + val originalBitmap = (image as? BitmapImage)?.bitmap ?: return 0.3f + + val width = originalBitmap.width + val height = originalBitmap.height + if (width == 0 || height == 0) return 0.3f + + // HARDWARE bitmap 不支持像素访问,需要复制为 ARGB_8888 + val bitmap = if (originalBitmap.config == Bitmap.Config.HARDWARE) { + originalBitmap.copy(Bitmap.Config.ARGB_8888, false) ?: return 0.3f + } else { + originalBitmap + } + + try { + // 采样步长:最多采样 ~400 个像素点(20x20 网格) + val stepX = maxOf(1, width / 20) + val stepY = maxOf(1, height / 20) + + var totalLuminance = 0.0 + var count = 0 + + var y = 0 + while (y < height) { + var x = 0 + while (x < width) { + val pixel = bitmap.getPixel(x, y) + val r = (pixel shr 16 and 0xFF) / 255.0 + val g = (pixel shr 8 and 0xFF) / 255.0 + val b = (pixel and 0xFF) / 255.0 + + // 感知亮度公式 + totalLuminance += 0.299 * r + 0.587 * g + 0.114 * b + count++ + + x += stepX + } + y += stepY + } + + return if (count > 0) (totalLuminance / count).toFloat() else 0.3f + } finally { + if (bitmap !== originalBitmap) { + bitmap.recycle() + } + } +} diff --git a/shared/ui/src/commonMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.kt b/shared/ui/src/commonMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.kt new file mode 100644 index 000000000..61229481b --- /dev/null +++ b/shared/ui/src/commonMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.kt @@ -0,0 +1,66 @@ +package com.xiaoyv.bangumi.shared.ui.component.image + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import coil3.compose.AsyncImagePainter + +/** + * 用于背景区域根据模糊背景图的明暗自动决定文字颜色。 + * + * - 背景偏暗 → 文字为白色 + * - 背景偏亮 → 文字为黑色 + * + * 使用方式: + * ``` + * val imageColorState = rememberImageColorState() + * BlurImage(onState = imageColorState.onImageState, ...) + * Text(color = imageColorState.contentColor, ...) + * ``` + */ +@Stable +class ImageColorState { + /** + * 背景上文字应使用的颜色 + */ + var contentColor: Color by mutableStateOf(Color.White) + private set + + /** + * 背景上次要文字应使用的颜色(带透明度) + */ + val contentColorSecondary: Color + get() = contentColor.copy(alpha = 0.75f) + + /** + * 传递给 BlurImage 的 onState 回调 + */ + val onImageState: (AsyncImagePainter.State) -> Unit = { state -> + if (state is AsyncImagePainter.State.Success) { + val image = state.result.image + val luminance = computeAverageLuminance(image) + // 亮度阈值:0.5 以下认为是深色背景,使用白色文字 + contentColor = if (luminance < 0.5f) Color.White else Color.Black + } + } +} + +/** + * 记住一个 ImageColorState 实例 + */ +@Composable +fun rememberImageColorState(): ImageColorState { + return remember { ImageColorState() } +} + +/** + * 从 Coil3 Image 中计算平均亮度(0.0 ~ 1.0) + * 使用相对亮度公式: L = 0.299*R + 0.587*G + 0.114*B + * + * 平台实现需要从 coil3.Image 中提取像素数据进行计算 + */ +expect fun computeAverageLuminance(image: coil3.Image): Float diff --git a/shared/ui/src/iosMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.ios.kt b/shared/ui/src/iosMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.ios.kt new file mode 100644 index 000000000..04a48f48f --- /dev/null +++ b/shared/ui/src/iosMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.ios.kt @@ -0,0 +1,44 @@ +package com.xiaoyv.bangumi.shared.ui.component.image + +import coil3.BitmapImage +import coil3.Image + +/** + * iOS 平台实现:从 Coil3 Image 中提取平均亮度 + * + * 通过 Skia Bitmap 采样像素计算平均亮度 + */ +actual fun computeAverageLuminance(image: Image): Float { + val bitmap = (image as? BitmapImage)?.bitmap ?: return 0.3f + + val width = bitmap.width + val height = bitmap.height + if (width == 0 || height == 0) return 0.3f + + // 采样步长:最多采样 ~400 个像素点(20x20 网格) + val stepX = maxOf(1, width / 20) + val stepY = maxOf(1, height / 20) + + var totalLuminance = 0.0 + var count = 0 + + var y = 0 + while (y < height) { + var x = 0 + while (x < width) { + val color = bitmap.getColor(x, y) + val r = ((color shr 16) and 0xFF) / 255.0 + val g = ((color shr 8) and 0xFF) / 255.0 + val b = (color and 0xFF) / 255.0 + + // 感知亮度公式 + totalLuminance += 0.299 * r + 0.587 * g + 0.114 * b + count++ + + x += stepX + } + y += stepY + } + + return if (count > 0) (totalLuminance / count).toFloat() else 0.3f +} diff --git a/shared/ui/src/jvmMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.jvm.kt b/shared/ui/src/jvmMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.jvm.kt new file mode 100644 index 000000000..1edaccbc9 --- /dev/null +++ b/shared/ui/src/jvmMain/kotlin/com/xiaoyv/bangumi/shared/ui/component/image/ImageColorState.jvm.kt @@ -0,0 +1,44 @@ +package com.xiaoyv.bangumi.shared.ui.component.image + +import coil3.BitmapImage +import coil3.Image + +/** + * JVM 平台实现:从 Coil3 Image 中提取平均亮度 + * + * 通过 Skia Bitmap 采样像素计算平均亮度 + */ +actual fun computeAverageLuminance(image: Image): Float { + val bitmap = (image as? BitmapImage)?.bitmap ?: return 0.3f + + val width = bitmap.width + val height = bitmap.height + if (width == 0 || height == 0) return 0.3f + + // 采样步长:最多采样 ~400 个像素点(20x20 网格) + val stepX = maxOf(1, width / 20) + val stepY = maxOf(1, height / 20) + + var totalLuminance = 0.0 + var count = 0 + + var y = 0 + while (y < height) { + var x = 0 + while (x < width) { + val color = bitmap.getColor(x, y) + val r = ((color shr 16) and 0xFF) / 255.0 + val g = ((color shr 8) and 0xFF) / 255.0 + val b = (color and 0xFF) / 255.0 + + // 感知亮度公式 + totalLuminance += 0.299 * r + 0.587 * g + 0.114 * b + count++ + + x += stepX + } + y += stepY + } + + return if (count > 0) (totalLuminance / count).toFloat() else 0.3f +} From 20ad026017b7d50e2508fc5ba06b81860810a466 Mon Sep 17 00:00:00 2001 From: zimya <57585727+zimya@users.noreply.github.com> Date: Sat, 30 May 2026 18:16:55 +0800 Subject: [PATCH 4/4] Fix: incorrect main screen tab color on night mode. --- .../bangumi/features/main/MainScreen.kt | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/features/main/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/MainScreen.kt b/features/main/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/MainScreen.kt index 669ad9823..5967984a2 100644 --- a/features/main/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/MainScreen.kt +++ b/features/main/src/commonMain/kotlin/com/xiaoyv/bangumi/features/main/MainScreen.kt @@ -74,17 +74,33 @@ fun MainScreen( NavigationSuiteScaffold( navigationSuiteItems = { bottomTabs.forEach { item -> + val selected = backStack.current == item.first item( modifier = Modifier.padding(bottom = if (isWideScreen) LayoutPadding else 0.dp), label = { Text( text = stringResource(item.second.label), maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + color = if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } ) }, - icon = { Icon(item.second.icon, stringResource(item.second.label)) }, - selected = backStack.current == item.first, + icon = { + Icon( + item.second.icon, + stringResource(item.second.label), + tint = if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + ) + }, + selected = selected, badge = { val appState = LocalSharedState.current val unreadCnt = appState.unreadNotification + appState.unreadMessage