feat: about page with dynami blur

This commit is contained in:
Miuzarte
2026-04-10 16:30:23 +08:00
parent da2bb9e227
commit 1a270ae2dd
14 changed files with 795 additions and 64 deletions

View File

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

View File

@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="top.yukonga.miuix.kmp.blur" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -30,4 +34,4 @@
</application>
</manifest>
</manifest>

View File

@@ -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<BlendColorEntry>,
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<BlendColorEntry> =
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<BlendColorEntry> =
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);
}
"""

View File

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

View File

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

View File

@@ -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 = "返回"
)
}

View File

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

View File

@@ -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 = "返回"
)
}

View File

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

View File

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

View File

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