From 1a270ae2dd6bea61e8c3a4d6450962ea0b1fa8cd Mon Sep 17 00:00:00 2001
From: Miuzarte <982809597@qq.com>
Date: Fri, 10 Apr 2026 16:30:23 +0800
Subject: [PATCH] feat: about page with dynami blur
---
README.md | 4 +-
TODO.md | 3 -
app/build.gradle.kts | 1 +
app/src/main/AndroidManifest.xml | 8 +-
.../scrcpyforandroid/pages/AboutScreen.kt | 744 ++++++++++++++++++
.../scrcpyforandroid/pages/DeviceTabScreen.kt | 6 +-
.../scrcpyforandroid/pages/MainScreen.kt | 8 +
.../pages/ScrcpyAllOptionsScreen.kt | 4 +-
.../scrcpyforandroid/pages/SettingsScreen.kt | 61 +-
.../pages/VirtualButtonOrderScreen.kt | 4 +-
.../services/AppUpdateChecker.kt | 3 +-
.../widgets/SectionSmallTitle.kt | 8 -
.../widgets/VirtualButtons.kt | 4 +-
gradle/libs.versions.toml | 1 +
14 files changed, 795 insertions(+), 64 deletions(-)
create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt
diff --git a/README.md b/README.md
index dc6c9a8..ca84b1b 100644
--- a/README.md
+++ b/README.md
@@ -41,8 +41,8 @@
## 构建
- JDK 17+
-- Android SDK(含 `compileSdk 37` / `buildTools 36.0.0`)
-- Android NDK `28.2.13676358`
+- Android SDK (`compileSdk 37` / `buildTools 37.0.0`)
+- Android NDK `29.0.14206865`
```bash
./gradlew assembleDebug
diff --git a/TODO.md b/TODO.md
index da2609f..531f5c8 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,8 +1,5 @@
# TODO
-- miuix v0.9
-- 在线更新检查
-
## PARAMS
- orientation locking
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6d01866..7736ead 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -88,6 +88,7 @@ dependencies {
implementation(libs.androidx.navigation3.runtime)
implementation(libs.material)
implementation(libs.miuix.ui)
+ implementation(libs.miuix.blur)
implementation(libs.miuix.preference)
implementation(libs.miuix.icons)
implementation(libs.miuix.navigation3.ui)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d8f7def..886b7ad 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,5 +1,9 @@
-
+
+
+
@@ -30,4 +34,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt
new file mode 100644
index 0000000..3c8ac8f
--- /dev/null
+++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt
@@ -0,0 +1,744 @@
+package io.github.miuzarte.scrcpyforandroid.pages
+
+import android.content.Intent
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+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.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.luminance
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.rememberTextMeasurer
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.net.toUri
+import io.github.miuzarte.scrcpyforandroid.BuildConfig
+import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker
+import kotlinx.coroutines.flow.onEach
+import top.yukonga.miuix.kmp.basic.Card
+import top.yukonga.miuix.kmp.basic.CardDefaults
+import top.yukonga.miuix.kmp.basic.Icon
+import top.yukonga.miuix.kmp.basic.IconButton
+import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
+import top.yukonga.miuix.kmp.basic.Scaffold
+import top.yukonga.miuix.kmp.basic.SmallTopAppBar
+import top.yukonga.miuix.kmp.basic.Text
+import top.yukonga.miuix.kmp.blur.BlendColorEntry
+import top.yukonga.miuix.kmp.blur.BlurBlendMode
+import top.yukonga.miuix.kmp.blur.BlurColors
+import top.yukonga.miuix.kmp.blur.BlurDefaults
+import top.yukonga.miuix.kmp.blur.LayerBackdrop
+import top.yukonga.miuix.kmp.blur.RuntimeShader
+import top.yukonga.miuix.kmp.blur.asBrush
+import top.yukonga.miuix.kmp.blur.isRenderEffectSupported
+import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported
+import top.yukonga.miuix.kmp.blur.layerBackdrop
+import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop
+import top.yukonga.miuix.kmp.blur.textureBlur
+import top.yukonga.miuix.kmp.preference.ArrowPreference
+import top.yukonga.miuix.kmp.shapes.SmoothRoundedCornerShape
+import top.yukonga.miuix.kmp.theme.MiuixTheme
+import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
+import androidx.compose.ui.graphics.BlendMode as ComposeBlendMode
+
+@Composable
+internal fun AboutScreen(
+ onBack: () -> Unit,
+) {
+ val topAppBarScrollBehavior = MiuixScrollBehavior()
+ val lazyListState = rememberLazyListState()
+ var logoHeightPx by remember { mutableIntStateOf(0) }
+
+ val scrollProgress by remember {
+ derivedStateOf {
+ if (logoHeightPx <= 0) {
+ 0f
+ } else {
+ val index = lazyListState.firstVisibleItemIndex
+ val offset = lazyListState.firstVisibleItemScrollOffset
+ if (index > 0) 1f else (offset.toFloat() / logoHeightPx).coerceIn(0f, 1f)
+ }
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ SmallTopAppBar(
+ title = "关于",
+ scrollBehavior = topAppBarScrollBehavior,
+ color = colorScheme.surface.copy(alpha = if (scrollProgress == 1f) 1f else 0f),
+ titleColor = colorScheme.onSurface.copy(alpha = scrollProgress),
+ defaultWindowInsetsPadding = false,
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = "返回",
+ )
+ }
+ },
+ )
+ },
+ ) { innerPadding ->
+ AboutContent(
+ padding = PaddingValues(
+ top = innerPadding.calculateTopPadding(),
+ bottom = innerPadding.calculateBottomPadding(),
+ ),
+ lazyListState = lazyListState,
+ scrollProgress = scrollProgress,
+ onLogoHeightChanged = { logoHeightPx = it },
+ )
+ }
+}
+
+@Composable
+private fun AboutContent(
+ padding: PaddingValues,
+ lazyListState: androidx.compose.foundation.lazy.LazyListState,
+ scrollProgress: Float,
+ onLogoHeightChanged: (Int) -> Unit,
+) {
+ val context = LocalContext.current
+ val updateState by AppUpdateChecker.state.collectAsState()
+ val backdrop = rememberLayerBackdrop()
+ val blurEnabled = remember { isRenderEffectSupported() }
+ val isDark = MiuixTheme.colorScheme.background.luminance() < 0.5f
+ val cardBlendColors = remember(isDark) { aboutCardBlendToken(isDark) }
+ var logoAreaY by remember { mutableFloatStateOf(0f) }
+ var projectNameY by remember { mutableFloatStateOf(0f) }
+ var versionCodeY by remember { mutableFloatStateOf(0f) }
+ var projectNameProgress by remember { mutableFloatStateOf(0f) }
+ var versionCodeProgress by remember { mutableFloatStateOf(0f) }
+ var initialLogoAreaY by remember { mutableFloatStateOf(0f) }
+
+ val (releaseStatusText, releasesUrl) = remember(updateState) {
+ when (val state = updateState) {
+ AppUpdateChecker.State.Idle -> null to AppUpdateChecker.RELEASES_URL
+ AppUpdateChecker.State.Checking -> "..." to AppUpdateChecker.RELEASES_URL
+ is AppUpdateChecker.State.Ready -> state.release.latestVersion to state.release.htmlUrl
+ is AppUpdateChecker.State.Error -> "错误" to AppUpdateChecker.RELEASES_URL
+ }
+ }
+
+ LaunchedEffect(lazyListState) {
+ snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
+ .onEach { offset ->
+ if (lazyListState.firstVisibleItemIndex > 0) {
+ projectNameProgress = 1f
+ versionCodeProgress = 1f
+ return@onEach
+ }
+ if (initialLogoAreaY == 0f && logoAreaY > 0f) {
+ initialLogoAreaY = logoAreaY
+ }
+ val refLogoAreaY = if (initialLogoAreaY > 0f) initialLogoAreaY else logoAreaY
+ val stage1TotalLength = refLogoAreaY - versionCodeY
+ val stage2TotalLength = versionCodeY - projectNameY
+ val versionCodeDelay = stage1TotalLength * 0.5f
+ versionCodeProgress =
+ ((offset.toFloat() - versionCodeDelay) / (stage1TotalLength - versionCodeDelay).coerceAtLeast(
+ 1f
+ ))
+ .coerceIn(0f, 1f)
+ projectNameProgress =
+ ((offset.toFloat() - stage1TotalLength) / stage2TotalLength.coerceAtLeast(1f))
+ .coerceIn(0f, 1f)
+ }
+ .collect {}
+ }
+
+ BgEffectBackground(
+ dynamicBackground = true,
+ modifier = Modifier.fillMaxSize(),
+ bgModifier = Modifier.layerBackdrop(backdrop),
+ effectBackground = true,
+ alpha = { 1f - scrollProgress },
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ top = padding.calculateTopPadding() + 92.dp,
+ start = 24.dp,
+ end = 24.dp,
+ ),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ BoxWithConstraints(
+ modifier = Modifier.fillMaxWidth(),
+ contentAlignment = Alignment.Center,
+ ) {
+ val density = LocalDensity.current
+ val textMeasurer = rememberTextMeasurer()
+ val baseTitleFontSize = 35.sp
+ val titleLayout = remember(textMeasurer) {
+ textMeasurer.measure(
+ text = "Scrcpy for Android",
+ style = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = baseTitleFontSize,
+ ),
+ softWrap = false,
+ )
+ }
+ val titleFontSize = with(density) {
+ val availableWidthPx = maxWidth.roundToPx().toFloat()
+ val measuredWidthPx = titleLayout.size.width.toFloat().coerceAtLeast(1f)
+ val scale = (availableWidthPx / measuredWidthPx).coerceAtMost(1f)
+ (baseTitleFontSize.value * scale).coerceAtLeast(24f).sp
+ }
+ Text(
+ text = "Scrcpy for Android",
+ modifier = Modifier
+ .padding(top = 12.dp, bottom = 5.dp)
+ .onGloballyPositioned { coordinates ->
+ if (projectNameY != 0f) return@onGloballyPositioned
+ val y = coordinates.positionInWindow().y
+ val size = coordinates.size
+ projectNameY = y + size.height
+ }
+ .graphicsLayer {
+ alpha = 1f - projectNameProgress
+ scaleX = 1f - (projectNameProgress * 0.05f)
+ scaleY = 1f - (projectNameProgress * 0.05f)
+ }
+ .textureBlur(
+ backdrop = backdrop,
+ shape = SmoothRoundedCornerShape(16.dp),
+ blurRadius = 150f,
+ noiseCoefficient = BlurDefaults.NoiseCoefficient,
+ colors = BlurColors(
+ blendColors = heroBlendColors(isDark),
+ ),
+ contentBlendMode = ComposeBlendMode.DstIn,
+ enabled = blurEnabled,
+ ),
+ color = MiuixTheme.colorScheme.onBackground,
+ fontWeight = FontWeight.Bold,
+ fontSize = titleFontSize,
+ )
+ }
+ Text(
+ text = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
+ modifier = Modifier
+ .fillMaxWidth()
+ .graphicsLayer {
+ alpha = 1f - versionCodeProgress
+ scaleX = 1f - (versionCodeProgress * 0.05f)
+ scaleY = 1f - (versionCodeProgress * 0.05f)
+ }
+ .onGloballyPositioned { coordinates ->
+ if (versionCodeY != 0f) return@onGloballyPositioned
+ val y = coordinates.positionInWindow().y
+ val size = coordinates.size
+ versionCodeY = y + size.height
+ },
+ color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
+ fontSize = 14.sp,
+ textAlign = TextAlign.Center,
+ )
+ Text(
+ text = AppUpdateChecker.REPO_URL.removePrefix("https://"),
+ modifier = Modifier
+ .fillMaxWidth()
+ .graphicsLayer {
+ alpha = 1f - versionCodeProgress
+ scaleX = 1f - (versionCodeProgress * 0.05f)
+ scaleY = 1f - (versionCodeProgress * 0.05f)
+ }
+ .onGloballyPositioned { coordinates ->
+ if (versionCodeY != 0f) return@onGloballyPositioned
+ val y = coordinates.positionInWindow().y
+ val size = coordinates.size
+ versionCodeY = y + size.height
+ },
+ color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
+ fontSize = 14.sp,
+ textAlign = TextAlign.Center,
+ )
+ }
+
+ LazyColumn(
+ state = lazyListState,
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(
+ top = padding.calculateTopPadding(),
+ bottom = padding.calculateBottomPadding() + 16.dp,
+ ),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ item(key = "logoSpacer") {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(320.dp)
+ .padding(top = 36.dp)
+ .onSizeChanged { onLogoHeightChanged(it.height) }
+ .onGloballyPositioned { coordinates ->
+ val y = coordinates.positionInWindow().y
+ val size = coordinates.size
+ logoAreaY = y + size.height
+ },
+ contentAlignment = Alignment.TopCenter,
+ ) {}
+ }
+ item(key = "about") {
+ Column(
+ modifier = Modifier
+ .fillParentMaxHeight()
+ .padding(bottom = padding.calculateBottomPadding()),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ AboutCard(
+ modifier = Modifier.padding(horizontal = 12.dp),
+ backdrop = backdrop,
+ blurEnabled = blurEnabled,
+ blendColors = cardBlendColors,
+ ) {
+ ArrowPreference(
+ title = "项目仓库",
+ endActions = {
+ Text(
+ text = "GitHub",
+ fontSize = MiuixTheme.textStyles.body2.fontSize,
+ color = colorScheme.onSurfaceVariantActions,
+ )
+ },
+ onClick = {
+ context.startActivity(
+ Intent(Intent.ACTION_VIEW, AppUpdateChecker.REPO_URL.toUri())
+ )
+ },
+ )
+ ArrowPreference(
+ title = "版本发布",
+ endActions = {
+ releaseStatusText?.let {
+ Text(
+ text = it,
+ fontSize = MiuixTheme.textStyles.body2.fontSize,
+ color = colorScheme.onSurfaceVariantActions,
+ )
+ }
+ },
+ onClick = {
+ context.startActivity(
+ Intent(Intent.ACTION_VIEW, releasesUrl.toUri())
+ )
+ },
+ )
+ }
+ AboutCard(
+ modifier = Modifier.padding(horizontal = 12.dp),
+ backdrop = backdrop,
+ blurEnabled = blurEnabled,
+ blendColors = cardBlendColors,
+ ) {
+ ArrowPreference(
+ title = "License",
+ endActions = {
+ Text(
+ text = "Apache-2.0",
+ fontSize = MiuixTheme.textStyles.body2.fontSize,
+ color = colorScheme.onSurfaceVariantActions,
+ )
+ },
+ onClick = {
+ context.startActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ "https://www.apache.org/licenses/LICENSE-2.0.txt".toUri()
+ )
+ )
+ },
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun AboutCard(
+ modifier: Modifier,
+ backdrop: LayerBackdrop,
+ blurEnabled: Boolean,
+ blendColors: List,
+ content: @Composable () -> Unit,
+) {
+ Card(
+ modifier = modifier.textureBlur(
+ backdrop = backdrop,
+ shape = SmoothRoundedCornerShape(16.dp),
+ blurRadius = 60f,
+ noiseCoefficient = BlurDefaults.NoiseCoefficient,
+ colors = BlurColors(blendColors = blendColors),
+ enabled = blurEnabled,
+ ),
+ colors = CardDefaults.defaultColors(
+ if (blurEnabled) Color.Transparent else MiuixTheme.colorScheme.surfaceContainer,
+ Color.Transparent,
+ ),
+ ) {
+ content()
+ }
+}
+
+@Composable
+private fun heroBlendColors(isDark: Boolean): List =
+ remember(isDark) {
+ if (isDark) {
+ listOf(
+ BlendColorEntry(Color(0xE6A1A1A1), BlurBlendMode.ColorDodge),
+ BlendColorEntry(Color(0x4DE6E6E6), BlurBlendMode.LinearLight),
+ BlendColorEntry(Color(0xFF1AF500), BlurBlendMode.Lab),
+ )
+ } else {
+ listOf(
+ BlendColorEntry(Color(0xCC4A4A4A), BlurBlendMode.ColorBurn),
+ BlendColorEntry(Color(0xFF4F4F4F), BlurBlendMode.LinearLight),
+ BlendColorEntry(Color(0xFF1AF200), BlurBlendMode.Lab),
+ )
+ }
+ }
+
+private fun aboutCardBlendToken(isDark: Boolean): List =
+ if (isDark) {
+ listOf(
+ BlendColorEntry(Color(0x4DA9A9A9), BlurBlendMode.Luminosity),
+ BlendColorEntry(Color(0x1A9C9C9C), BlurBlendMode.PlusDarker),
+ )
+ } else {
+ listOf(
+ BlendColorEntry(Color(0x340034F9), BlurBlendMode.Overlay),
+ BlendColorEntry(Color(0xB3FFFFFF), BlurBlendMode.HardLight),
+ )
+ }
+
+@Composable
+private inline fun BgEffectBackground(
+ dynamicBackground: Boolean,
+ modifier: Modifier = Modifier,
+ bgModifier: Modifier = Modifier,
+ effectBackground: Boolean = true,
+ crossinline alpha: () -> Float = { 1f },
+ content: @Composable BoxScope.() -> Unit,
+) {
+ val shaderSupported = remember { isRuntimeShaderSupported() }
+ if (!shaderSupported) {
+ Box(modifier = modifier, content = content)
+ return
+ }
+ Box(modifier = modifier) {
+ val surface = colorScheme.surface
+ val painter = remember { BgEffectPainter() }
+ val animTime = rememberFrameTimeSeconds(dynamicBackground)
+ val isDark = colorScheme.background.luminance() < 0.5f
+
+ Canvas(
+ modifier = Modifier
+ .fillMaxSize()
+ .then(bgModifier),
+ ) {
+ drawRect(surface)
+ if (effectBackground) {
+ val drawHeight = size.height * 0.78f
+ painter.updateResolution(size.width, size.height)
+ painter.updatePresetIfNeeded(drawHeight, size.height, size.width, isDark)
+ painter.updateAnimTime(animTime())
+ drawRect(painter.runtimeShader.asBrush(), alpha = alpha())
+ }
+ }
+ content()
+ }
+}
+
+@Composable
+private fun rememberFrameTimeSeconds(
+ playing: Boolean = true,
+): () -> Float {
+ var time by remember { mutableFloatStateOf(0f) }
+ var startOffset by remember { mutableFloatStateOf(0f) }
+
+ LaunchedEffect(playing) {
+ if (!playing) {
+ startOffset = time
+ return@LaunchedEffect
+ }
+ val start = withFrameNanos { it }
+ while (playing) {
+ val now = withFrameNanos { it }
+ time = startOffset + (now - start) / 1_000_000_000f
+ }
+ }
+ return { time }
+}
+
+// @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+private class BgEffectPainter {
+ val runtimeShader by lazy {
+ RuntimeShader(OS2_BG_FRAG).also { initStaticUniforms(it) }
+ }
+ private val resolution = FloatArray(2)
+ private val bound = FloatArray(4)
+ private var animTime = Float.NaN
+ private var isDarkCached: Boolean? = null
+ private var presetApplied = false
+
+ private fun initStaticUniforms(shader: RuntimeShader) {
+ shader.setFloatUniform("uTranslateY", 0f)
+ shader.setFloatUniform("uNoiseScale", 1.5f)
+ shader.setFloatUniform("uPointOffset", 0.1f)
+ shader.setFloatUniform("uPointRadiusMulti", 1f)
+ shader.setFloatUniform("uAlphaMulti", 1f)
+ }
+
+ fun updateResolution(width: Float, height: Float) {
+ if (resolution[0] == width && resolution[1] == height) return
+ resolution[0] = width
+ resolution[1] = height
+ runtimeShader.setFloatUniform("uResolution", resolution)
+ }
+
+ fun updateAnimTime(time: Float) {
+ if (animTime == time) return
+ animTime = time
+ runtimeShader.setFloatUniform("uAnimTime", animTime)
+ }
+
+ fun updatePresetIfNeeded(
+ logoHeight: Float,
+ height: Float,
+ width: Float,
+ isDark: Boolean,
+ ) {
+ if (presetApplied && isDarkCached == isDark) return
+ updateBound(logoHeight, height, width)
+ val preset = if (isDark) PHONE_DARK else PHONE_LIGHT
+ runtimeShader.setFloatUniform("uPoints", preset.points)
+ runtimeShader.setFloatUniform("uColors", preset.colors)
+ runtimeShader.setFloatUniform("uLightOffset", preset.lightOffset)
+ runtimeShader.setFloatUniform("uSaturateOffset", preset.saturateOffset)
+ runtimeShader.setFloatUniform("uBound", bound)
+ isDarkCached = isDark
+ presetApplied = true
+ }
+
+ private fun updateBound(
+ logoHeight: Float,
+ totalHeight: Float,
+ totalWidth: Float,
+ ) {
+ val heightRatio = logoHeight / totalHeight
+ if (totalWidth <= totalHeight) {
+ bound[0] = 0f
+ bound[1] = 1f - heightRatio
+ bound[2] = 1f
+ bound[3] = heightRatio
+ } else {
+ val aspectRatio = totalWidth / totalHeight
+ val contentCenterY = 1f - heightRatio / 2f
+ bound[0] = 0f
+ bound[1] = contentCenterY - aspectRatio / 2f
+ bound[2] = 1f
+ bound[3] = aspectRatio
+ }
+ }
+
+ private data class BgEffectPreset(
+ val points: FloatArray,
+ val colors: FloatArray,
+ val lightOffset: Float,
+ val saturateOffset: Float,
+ ) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as BgEffectPreset
+
+ if (lightOffset != other.lightOffset) return false
+ if (saturateOffset != other.saturateOffset) return false
+ if (!points.contentEquals(other.points)) return false
+ if (!colors.contentEquals(other.colors)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = lightOffset.hashCode()
+ result = 31 * result + saturateOffset.hashCode()
+ result = 31 * result + points.contentHashCode()
+ result = 31 * result + colors.contentHashCode()
+ return result
+ }
+ }
+
+ private companion object {
+ val PHONE_LIGHT = BgEffectPreset(
+ points = floatArrayOf(
+ 0.67f, 0.42f, 1.0f, 0.69f, 0.75f, 1.0f,
+ 0.14f, 0.71f, 0.95f, 0.14f, 0.27f, 0.8f,
+ ),
+ colors = floatArrayOf(
+ 0.57f, 0.76f, 0.98f, 1.0f,
+ 0.98f, 0.85f, 0.68f, 1.0f,
+ 0.98f, 0.75f, 0.93f, 1.0f,
+ 0.73f, 0.70f, 0.98f, 1.0f,
+ ),
+ lightOffset = 0.1f,
+ saturateOffset = 0.2f,
+ )
+ val PHONE_DARK = BgEffectPreset(
+ points = floatArrayOf(
+ 0.63f, 0.50f, 0.88f, 0.69f, 0.75f, 0.80f,
+ 0.17f, 0.66f, 0.81f, 0.14f, 0.24f, 0.72f,
+ ),
+ colors = floatArrayOf(
+ 0.0f, 0.31f, 0.58f, 1.0f,
+ 0.53f, 0.29f, 0.15f, 1.0f,
+ 0.46f, 0.06f, 0.27f, 1.0f,
+ 0.16f, 0.12f, 0.45f, 1.0f,
+ ),
+ lightOffset = -0.1f,
+ saturateOffset = 0.2f,
+ )
+ }
+}
+
+private const val OS2_BG_FRAG = """
+ uniform vec2 uResolution;
+ uniform shader uTex;
+ uniform float uAnimTime;
+ uniform vec4 uBound;
+ uniform float uTranslateY;
+ uniform vec3 uPoints[4];
+ uniform vec4 uColors[4];
+ uniform float uAlphaMulti;
+ uniform float uNoiseScale;
+ uniform float uPointOffset;
+ uniform float uPointRadiusMulti;
+ uniform float uSaturateOffset;
+ uniform float uLightOffset;
+
+ vec3 rgb2hsv(vec3 c)
+ {
+ vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
+ vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
+ vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
+ float d = q.x - min(q.w, q.y);
+ float e = 1.0e-10;
+ return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
+ }
+
+ vec3 hsv2rgb(vec3 c)
+ {
+ vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+ vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+ return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+ }
+
+ float hash(vec2 p) {
+ vec3 p3 = fract(vec3(p.xyx) * 0.13);
+ p3 += dot(p3, p3.yzx + 3.333);
+ return fract((p3.x + p3.y) * p3.z);
+ }
+
+ float perlin(vec2 x) {
+ vec2 i = floor(x);
+ vec2 f = fract(x);
+ float a = hash(i);
+ float b = hash(i + vec2(1.0, 0.0));
+ float c = hash(i + vec2(0.0, 1.0));
+ float d = hash(i + vec2(1.0, 1.0));
+ vec2 u = f * f * (3.0 - 2.0 * f);
+ return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
+ }
+
+ float gradientNoise(in vec2 uv)
+ {
+ return fract(52.9829189 * fract(dot(uv, vec2(0.06711056, 0.00583715))));
+ }
+
+ vec4 main(vec2 fragCoord){
+ vec2 vUv = fragCoord/uResolution;
+ vUv.y = 1.0-vUv.y;
+ vec2 uv = vUv;
+ uv -= vec2(0., uTranslateY);
+ uv.xy -= uBound.xy;
+ uv.xy /= uBound.zw;
+ vec3 hsv;
+ vec4 color = vec4(0.0);
+ float noiseValue = perlin(vUv * uNoiseScale + vec2(-uAnimTime, -uAnimTime));
+
+ for (int i = 0; i < 4; i++){
+ vec4 pointColor = uColors[i];
+ pointColor.rgb *= pointColor.a;
+ vec2 point = uPoints[i].xy;
+ float rad = uPoints[i].z * uPointRadiusMulti;
+ point.x += sin(uAnimTime + point.y) * uPointOffset;
+ point.y += cos(uAnimTime + point.x) * uPointOffset;
+ float d = distance(uv, point);
+ float pct = smoothstep(rad, 0., d);
+ color.rgb = mix(color.rgb, pointColor.rgb, pct);
+ color.a = mix(color.a, pointColor.a, pct);
+ }
+
+ float oppositeNoise = smoothstep(0., 1., noiseValue);
+ if (color.a > 0.001) {
+ color.rgb /= color.a;
+ } else {
+ color.rgb = vec3(0.0);
+ }
+ hsv = rgb2hsv(color.rgb);
+ hsv.y = mix(hsv.y, 0.0, oppositeNoise * uSaturateOffset);
+ color.rgb = hsv2rgb(hsv);
+ color.rgb += oppositeNoise * uLightOffset;
+ color.a = clamp(color.a, 0., 1.);
+ color.a *= uAlphaMulti;
+
+ vec4 uiColor = uTex.eval(vec2(vUv.x, 1.0 - vUv.y)*uResolution);
+ vec4 fragColor;
+ color.rgb += (10.0 / 255.0) * gradientNoise(fragCoord.xy) - (5.0 / 255.0);
+ if (uiColor.a < 0.01) {
+ fragColor = color;
+ } else {
+ fragColor = uiColor;
+ }
+ return vec4(fragColor.rgb*fragColor.a, fragColor.a);
+ }
+"""
diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt
index 9711966..77771a0 100644
--- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt
+++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt
@@ -833,7 +833,7 @@ fun DeviceTabPage(
}
item {
- SectionSmallTitle("无线配对", showLeadingSpacer = false)
+ SectionSmallTitle("无线配对")
// "使用配对码配对设备"
PairingCard(
busy = busy,
@@ -866,7 +866,7 @@ fun DeviceTabPage(
if (adbConnected) {
item {
- SectionSmallTitle("Scrcpy", showLeadingSpacer = false)
+ SectionSmallTitle("Scrcpy")
ConfigPanel(
busy = busy,
snackbar = snackbar,
@@ -970,7 +970,7 @@ fun DeviceTabPage(
if (EventLogger.hasLogs()) {
item {
- SectionSmallTitle("日志", showLeadingSpacer = false)
+ SectionSmallTitle("日志")
Card {
TextField(
value = EventLogger.eventLog.joinToString(separator = "\n"),
diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt
index 0cc58fa..58cf356 100644
--- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt
+++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt
@@ -83,6 +83,7 @@ private enum class MainBottomTabDestination(
sealed interface RootScreen : NavKey {
data object Home : RootScreen
data object Advanced : RootScreen
+ data object About : RootScreen
data object VirtualButtonOrder : RootScreen
data object Fullscreen : RootScreen
}
@@ -393,6 +394,7 @@ fun MainScreen() {
snackbar = snackbarController,
onOpenReorderDevices = { showReorderDevices = true },
onOpenVirtualButtonOrder = { rootBackStack.add(RootScreen.VirtualButtonOrder) },
+ onOpenAbout = { rootBackStack.add(RootScreen.About) },
onPickServer = {
picker.launch(
arrayOf(
@@ -423,6 +425,12 @@ fun MainScreen() {
)
}
+ entry(RootScreen.About) {
+ AboutScreen(
+ onBack = ::popRoot,
+ )
+ }
+
entry(RootScreen.VirtualButtonOrder) {
VirtualButtonOrderScreen(
onBack = ::popRoot,
diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt
index e776f01..c047c2a 100644
--- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt
+++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt
@@ -13,7 +13,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -81,7 +81,7 @@ internal fun ScrcpyAllOptionsScreen(
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
- Icons.AutoMirrored.Filled.ArrowBack,
+ Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = "返回"
)
}
diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt
index 3935314..0c1e657 100644
--- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt
+++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt
@@ -28,7 +28,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
-import androidx.core.net.toUri
import io.github.miuzarte.scrcpyforandroid.BuildConfig
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
@@ -68,6 +67,7 @@ fun SettingsScreen(
snackbar: SnackbarController,
onOpenReorderDevices: () -> Unit,
onOpenVirtualButtonOrder: () -> Unit,
+ onOpenAbout: () -> Unit,
onPickServer: () -> Unit,
) {
Scaffold(
@@ -84,6 +84,7 @@ fun SettingsScreen(
snackbar = snackbar,
onOpenReorderDevices = onOpenReorderDevices,
onOpenVirtualButtonOrder = onOpenVirtualButtonOrder,
+ onOpenAbout = onOpenAbout,
onPickServer = onPickServer,
)
}
@@ -96,6 +97,7 @@ fun SettingsPage(
snackbar: SnackbarController,
onOpenReorderDevices: () -> Unit,
onOpenVirtualButtonOrder: () -> Unit,
+ onOpenAbout: () -> Unit,
onPickServer: () -> Unit,
) {
val context = LocalContext.current
@@ -155,43 +157,31 @@ fun SettingsPage(
}
val updateSummary = remember(updateState) {
- when (val state = updateState) {
- AppUpdateChecker.State.Idle ->
- "当前版本 ${BuildConfig.VERSION_NAME}"
+ "当前版本 ${BuildConfig.VERSION_NAME}" + when (val state = updateState) {
+ AppUpdateChecker.State.Idle -> ""
+ AppUpdateChecker.State.Checking -> ",正在检查更新"
+ AppUpdateChecker.State.Error -> ",检查更新失败"
- AppUpdateChecker.State.Checking ->
- "当前版本 ${BuildConfig.VERSION_NAME},正在检查更新"
+ is AppUpdateChecker.State.Ready -> when {
+ state.release.hasUpdate ->
+ ",发现新版本 ${state.release.latestVersion}"
- is AppUpdateChecker.State.Ready -> {
- when {
- state.release.hasUpdate ->
- "当前版本 ${state.release.currentVersion},发现新版本 ${state.release.latestVersion}"
+ state.release.currentVersion == state.release.latestVersion.removePrefix("v")
+ || state.release.currentVersion == state.release.latestVersion ->
+ ",已是最新版本"
- state.release.currentVersion == state.release.latestVersion.removePrefix("v")
- || state.release.currentVersion == state.release.latestVersion ->
- "当前版本 ${state.release.currentVersion},已是最新版本"
-
- else ->
- "当前版本 ${state.release.currentVersion},高于最新发布版本 ${state.release.latestVersion}"
- }
+ else -> ",高于最新发布版本 ${state.release.latestVersion}"
}
}
}
- val updateUrl = remember(updateState) {
- when (val state = updateState) {
- is AppUpdateChecker.State.Ready -> state.release.htmlUrl
- else -> "https://github.com/Miuzarte/ScrcpyForAndroid/releases"
- }
- }
-
// 设置
LazyColumn(
contentPadding = contentPadding,
scrollBehavior = scrollBehavior,
) {
item {
- SectionSmallTitle("主题", showLeadingSpacer = false)
+ SectionSmallTitle("主题")
Card {
OverlayDropdownPreference(
title = "外观模式",
@@ -281,7 +271,7 @@ fun SettingsPage(
}
item {
- SectionSmallTitle("scrcpy-server", showLeadingSpacer = false)
+ SectionSmallTitle("scrcpy-server")
Card {
Column(
modifier = Modifier.padding(vertical = UiSpacing.Large),
@@ -388,7 +378,7 @@ fun SettingsPage(
}
item {
- SectionSmallTitle("ADB", showLeadingSpacer = false)
+ SectionSmallTitle("ADB")
Card {
Column(
modifier = Modifier.padding(vertical = UiSpacing.Large),
@@ -446,7 +436,7 @@ fun SettingsPage(
if (needMigration) item {
// 这部分应该不会显示出来,
// 应用启动时就会执行迁移与旧数据的删除
- SectionSmallTitle("应用", showLeadingSpacer = false)
+ SectionSmallTitle("应用")
Card {
ArrowPreference(
title = "恢复旧版本配置",
@@ -479,19 +469,12 @@ fun SettingsPage(
}
item {
- SectionSmallTitle("关于", showLeadingSpacer = false)
+ SectionSmallTitle("")
Card {
ArrowPreference(
- title = "仓库发布页",
- summary = "$updateSummary\n${AppUpdateChecker.REPO_URL.removePrefix("https://")}",
- onClick = {
- context.startActivity(
- Intent(
- Intent.ACTION_VIEW,
- updateUrl.toUri(),
- )
- )
- },
+ title = "关于",
+ summary = updateSummary,
+ onClick = onOpenAbout,
)
}
}
diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt
index a0376b1..b373e94 100644
--- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt
+++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt
@@ -2,7 +2,7 @@ package io.github.miuzarte.scrcpyforandroid.pages
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -48,7 +48,7 @@ internal fun VirtualButtonOrderScreen(
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
- Icons.AutoMirrored.Filled.ArrowBack,
+ Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = "返回"
)
}
diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppUpdateChecker.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppUpdateChecker.kt
index 05f9565..ed4ee39 100644
--- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppUpdateChecker.kt
+++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppUpdateChecker.kt
@@ -33,6 +33,7 @@ object AppUpdateChecker {
sealed interface State {
data object Idle : State
data object Checking : State
+ data object Error : State
data class Ready(val release: ReleaseInfo) : State
}
@@ -52,7 +53,7 @@ object AppUpdateChecker {
Log.WARN,
error
)
- _state.value = State.Idle
+ _state.value = State.Error
}
}
}
diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/SectionSmallTitle.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/SectionSmallTitle.kt
index e1df591..fb6cde2 100644
--- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/SectionSmallTitle.kt
+++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/SectionSmallTitle.kt
@@ -1,23 +1,15 @@
package io.github.miuzarte.scrcpyforandroid.widgets
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import top.yukonga.miuix.kmp.basic.SmallTitle
@Composable
fun SectionSmallTitle(
text: String,
- showLeadingSpacer: Boolean = true,
insideMargin: PaddingValues = PaddingValues(16.dp, 8.dp),
) {
- if (showLeadingSpacer) {
- Spacer(Modifier.height(UiSpacing.SectionTitleLeadingGap))
- }
SmallTitle(
text = text,
insideMargin = insideMargin,
diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt
index 9d51740..d53f005 100644
--- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt
+++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.VolumeDown
import androidx.compose.material.icons.automirrored.rounded.VolumeOff
import androidx.compose.material.icons.automirrored.rounded.VolumeUp
@@ -70,7 +70,7 @@ enum class VirtualButtonAction(
BACK(
"back",
"返回",
- Icons.AutoMirrored.Filled.ArrowBack,
+ Icons.AutoMirrored.Rounded.ArrowBack,
UiAndroidKeycodes.BACK
),
APP_SWITCH(
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f8b5cd0..8fecced 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -31,6 +31,7 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "a
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
miuix-ui = { group = "top.yukonga.miuix.kmp", name = "miuix-ui", version.ref = "miuix" }
+miuix-blur = { group = "top.yukonga.miuix.kmp", name = "miuix-blur", version.ref = "miuix" }
miuix-preference = { group = "top.yukonga.miuix.kmp", name = "miuix-preference", version.ref = "miuix" }
miuix-icons = { group = "top.yukonga.miuix.kmp", name = "miuix-icons", version.ref = "miuix" }
miuix-navigation3-ui = { group = "top.yukonga.miuix.kmp", name = "miuix-navigation3-ui", version.ref = "miuix" }