Stand alone theme configuration for webuiX

- Add secondary color interface: isSecondaryPage (bool)
This commit is contained in:
ShirkNeko
2025-05-14 16:29:27 +08:00
parent dee7cc6f2b
commit c057c16391
4 changed files with 284 additions and 7 deletions

View File

@@ -546,9 +546,6 @@ fun Context.loadDynamicColorState() {
ThemeConfig.useDynamicColor = enabled
}
/**
* webui X样式
*/
@Composable
private fun SystemBarStyle(
darkMode: Boolean,

View File

@@ -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()
)
}
)
}
}

View File

@@ -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)

View File

@@ -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) }