From c057c16391739b4e1969a04be8716790bee460f6 Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Wed, 14 May 2025 16:29:27 +0800 Subject: [PATCH] Stand alone theme configuration for webuiX - Add secondary color interface: isSecondaryPage (bool) --- .../java/com/sukisu/ultra/ui/theme/Theme.kt | 3 - .../com/sukisu/ultra/ui/webui/WebUITheme.kt | 275 ++++++++++++++++++ .../sukisu/ultra/ui/webui/WebUIXActivity.kt | 6 +- .../sukisu/ultra/ui/webui/WebViewInterface.kt | 7 + 4 files changed, 284 insertions(+), 7 deletions(-) create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUITheme.kt diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt index 00c83c5a..8e698776 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt @@ -546,9 +546,6 @@ fun Context.loadDynamicColorState() { ThemeConfig.useDynamicColor = enabled } -/** - * webui X样式 - */ @Composable private fun SystemBarStyle( darkMode: Boolean, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUITheme.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUITheme.kt new file mode 100644 index 00000000..8b9f773b --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUITheme.kt @@ -0,0 +1,275 @@ +package com.sukisu.ultra.ui.webui + +import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.paint +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.zIndex +import android.os.Build +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.updateTransition +import androidx.compose.ui.graphics.graphicsLayer +import coil.compose.AsyncImagePainter +import coil.compose.rememberAsyncImagePainter +import com.sukisu.ultra.ui.theme.ThemeConfig +import com.sukisu.ultra.ui.theme.Typography +import com.sukisu.ultra.ui.theme.loadCustomBackground + +// 提供界面类型的本地组合 +val LocalIsSecondaryScreen = staticCompositionLocalOf { false } + +/** + * WebUI专用主题配置 + */ +@Composable +fun WebUIXTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + isSecondaryScreen: Boolean = false, + content: @Composable () -> Unit +) { + val context = LocalContext.current + + LaunchedEffect(Unit) { + if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) { + context.loadCustomBackground() + ThemeConfig.backgroundImageLoaded = false + } + } + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + if (darkTheme) { + dynamicDarkColorScheme(context).let { scheme -> + if (isSecondaryScreen) { + scheme.copy( + background = scheme.surfaceContainerHighest, + surface = scheme.surfaceContainerHighest + ) + } else { + scheme.copy( + background = Color.Transparent, + surface = Color.Transparent + ) + } + } + } else { + dynamicLightColorScheme(context).let { scheme -> + if (isSecondaryScreen) { + scheme.copy( + background = scheme.surfaceContainerHighest, + surface = scheme.surfaceContainerHighest + ) + } else { + scheme.copy( + background = Color.Transparent, + surface = Color.Transparent + ) + } + } + } + } + darkTheme -> { + if (isSecondaryScreen) { + darkColorScheme().copy( + background = MaterialTheme.colorScheme.surfaceContainerHighest, + surface = MaterialTheme.colorScheme.surfaceContainerHighest + ) + } else { + darkColorScheme().copy( + background = Color.Transparent, + surface = Color.Transparent + ) + } + } + else -> { + if (isSecondaryScreen) { + lightColorScheme().copy( + background = MaterialTheme.colorScheme.surfaceContainerHighest, + surface = MaterialTheme.colorScheme.surfaceContainerHighest + ) + } else { + lightColorScheme().copy( + background = Color.Transparent, + surface = Color.Transparent + ) + } + } + } + + ConfigureSystemBars(darkTheme) + + val backgroundUri = remember { mutableStateOf(ThemeConfig.customBackgroundUri) } + + LaunchedEffect(ThemeConfig.customBackgroundUri) { + backgroundUri.value = ThemeConfig.customBackgroundUri + } + val bgImagePainter = backgroundUri.value?.let { + rememberAsyncImagePainter( + model = it, + onError = { + ThemeConfig.backgroundImageLoaded = false + }, + onSuccess = { + ThemeConfig.backgroundImageLoaded = true + ThemeConfig.isThemeChanging = false + } + ) + } + + // 背景透明度动画 + val transition = updateTransition( + targetState = ThemeConfig.backgroundImageLoaded, + label = "bgTransition" + ) + + val bgAlpha by transition.animateFloat( + label = "bgAlpha", + transitionSpec = { + spring( + dampingRatio = 0.8f, + stiffness = 300f + ) + } + ) { loaded -> if (loaded) 1f else 0f } + CompositionLocalProvider(LocalIsSecondaryScreen provides isSecondaryScreen) { + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + ) { + if (isSecondaryScreen) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + ) { + content() + } + } else { + Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(-2f) + .background(if (darkTheme) Color.Black else Color.White) + ) + + backgroundUri.value?.let { uri -> + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(-1f) + .alpha(bgAlpha) + ) { + bgImagePainter?.let { painter -> + Box( + modifier = Modifier + .fillMaxSize() + .paint( + painter = painter, + contentScale = ContentScale.Crop + ) + .graphicsLayer { + alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f + } + ) + } + Box( + modifier = Modifier + .fillMaxSize() + .background( + if (darkTheme) Color.Black.copy(alpha = 0.6f) + else Color.White.copy(alpha = 0.1f) + ) + ) + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.radialGradient( + colors = listOf( + Color.Transparent, + if (darkTheme) Color.Black.copy(alpha = 0.5f) + else Color.Black.copy(alpha = 0.2f) + ), + radius = 1200f + ) + ) + ) + } + } + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(1f) + ) { + content() + } + } + } + } + } +} + +/** + * 获取当前界面是否为二级界面 + */ +@Composable +fun isSecondaryScreen(): Boolean { + return LocalIsSecondaryScreen.current +} + +/** + * 配置WebUI的系统栏样式 + */ +@Composable +private fun ConfigureSystemBars( + darkMode: Boolean, + statusBarScrim: Color = Color.Transparent, + navigationBarScrim: Color = Color.Transparent +) { + val context = LocalContext.current + val activity = context as ComponentActivity + + SideEffect { + activity.enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto( + statusBarScrim.toArgb(), + statusBarScrim.toArgb() + ) { darkMode }, + navigationBarStyle = when { + darkMode -> SystemBarStyle.dark( + navigationBarScrim.toArgb() + ) + else -> SystemBarStyle.light( + navigationBarScrim.toArgb(), + navigationBarScrim.toArgb() + ) + } + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt index fe585442..a65fd79d 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt @@ -20,7 +20,6 @@ import com.dergoogler.mmrl.ui.component.Loading import com.dergoogler.mmrl.webui.screen.WebUIScreen import com.dergoogler.mmrl.webui.util.rememberWebUIOptions import com.sukisu.ultra.BuildConfig -import com.sukisu.ultra.ui.theme.KernelSUTheme import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -69,7 +68,7 @@ class WebUIXActivity : ComponentActivity() { val prefs = getSharedPreferences("settings", MODE_PRIVATE) setContent { - KernelSUTheme { + WebUIXTheme { var isLoading by remember { mutableStateOf(true) } LaunchedEffect(Platform.isAlive) { @@ -82,8 +81,7 @@ class WebUIXActivity : ComponentActivity() { if (isLoading) { Loading() - - return@KernelSUTheme + return@WebUIXTheme } val webDebugging = prefs.getBoolean("enable_web_debugging", false) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt index 8fca1c71..e23ca2a9 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt @@ -24,6 +24,7 @@ import com.sukisu.ultra.ui.util.controlKpmModule import com.sukisu.ultra.ui.util.listKpmModules import java.io.File import java.util.concurrent.CompletableFuture +import androidx.compose.runtime.Composable class WebViewInterface( wxOptions: WXOptions, @@ -36,6 +37,12 @@ class WebViewInterface( private val modDir get() = "/data/adb/modules/${modId.id}" + @Composable + @JavascriptInterface + fun isSecondaryPage(): Boolean { + return isSecondaryScreen() + } + @JavascriptInterface fun exec(cmd: String): String { return withNewRootShell(true) { ShellUtils.fastCmd(this, cmd) }