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" }