feat: about page with dynami blur
This commit is contained in:
@@ -41,8 +41,8 @@
|
|||||||
## 构建
|
## 构建
|
||||||
|
|
||||||
- JDK 17+
|
- JDK 17+
|
||||||
- Android SDK(含 `compileSdk 37` / `buildTools 36.0.0`)
|
- Android SDK (`compileSdk 37` / `buildTools 37.0.0`)
|
||||||
- Android NDK `28.2.13676358`
|
- Android NDK `29.0.14206865`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew assembleDebug
|
./gradlew assembleDebug
|
||||||
|
|||||||
3
TODO.md
3
TODO.md
@@ -1,8 +1,5 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
- miuix v0.9
|
|
||||||
- 在线更新检查
|
|
||||||
|
|
||||||
## PARAMS
|
## PARAMS
|
||||||
|
|
||||||
- orientation locking
|
- orientation locking
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.navigation3.runtime)
|
implementation(libs.androidx.navigation3.runtime)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.miuix.ui)
|
implementation(libs.miuix.ui)
|
||||||
|
implementation(libs.miuix.blur)
|
||||||
implementation(libs.miuix.preference)
|
implementation(libs.miuix.preference)
|
||||||
implementation(libs.miuix.icons)
|
implementation(libs.miuix.icons)
|
||||||
implementation(libs.miuix.navigation3.ui)
|
implementation(libs.miuix.navigation3.ui)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
"""
|
||||||
@@ -833,7 +833,7 @@ fun DeviceTabPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SectionSmallTitle("无线配对", showLeadingSpacer = false)
|
SectionSmallTitle("无线配对")
|
||||||
// "使用配对码配对设备"
|
// "使用配对码配对设备"
|
||||||
PairingCard(
|
PairingCard(
|
||||||
busy = busy,
|
busy = busy,
|
||||||
@@ -866,7 +866,7 @@ fun DeviceTabPage(
|
|||||||
|
|
||||||
if (adbConnected) {
|
if (adbConnected) {
|
||||||
item {
|
item {
|
||||||
SectionSmallTitle("Scrcpy", showLeadingSpacer = false)
|
SectionSmallTitle("Scrcpy")
|
||||||
ConfigPanel(
|
ConfigPanel(
|
||||||
busy = busy,
|
busy = busy,
|
||||||
snackbar = snackbar,
|
snackbar = snackbar,
|
||||||
@@ -970,7 +970,7 @@ fun DeviceTabPage(
|
|||||||
|
|
||||||
if (EventLogger.hasLogs()) {
|
if (EventLogger.hasLogs()) {
|
||||||
item {
|
item {
|
||||||
SectionSmallTitle("日志", showLeadingSpacer = false)
|
SectionSmallTitle("日志")
|
||||||
Card {
|
Card {
|
||||||
TextField(
|
TextField(
|
||||||
value = EventLogger.eventLog.joinToString(separator = "\n"),
|
value = EventLogger.eventLog.joinToString(separator = "\n"),
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ private enum class MainBottomTabDestination(
|
|||||||
sealed interface RootScreen : NavKey {
|
sealed interface RootScreen : NavKey {
|
||||||
data object Home : RootScreen
|
data object Home : RootScreen
|
||||||
data object Advanced : RootScreen
|
data object Advanced : RootScreen
|
||||||
|
data object About : RootScreen
|
||||||
data object VirtualButtonOrder : RootScreen
|
data object VirtualButtonOrder : RootScreen
|
||||||
data object Fullscreen : RootScreen
|
data object Fullscreen : RootScreen
|
||||||
}
|
}
|
||||||
@@ -393,6 +394,7 @@ fun MainScreen() {
|
|||||||
snackbar = snackbarController,
|
snackbar = snackbarController,
|
||||||
onOpenReorderDevices = { showReorderDevices = true },
|
onOpenReorderDevices = { showReorderDevices = true },
|
||||||
onOpenVirtualButtonOrder = { rootBackStack.add(RootScreen.VirtualButtonOrder) },
|
onOpenVirtualButtonOrder = { rootBackStack.add(RootScreen.VirtualButtonOrder) },
|
||||||
|
onOpenAbout = { rootBackStack.add(RootScreen.About) },
|
||||||
onPickServer = {
|
onPickServer = {
|
||||||
picker.launch(
|
picker.launch(
|
||||||
arrayOf(
|
arrayOf(
|
||||||
@@ -423,6 +425,12 @@ fun MainScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry(RootScreen.About) {
|
||||||
|
AboutScreen(
|
||||||
|
onBack = ::popRoot,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
entry(RootScreen.VirtualButtonOrder) {
|
entry(RootScreen.VirtualButtonOrder) {
|
||||||
VirtualButtonOrderScreen(
|
VirtualButtonOrderScreen(
|
||||||
onBack = ::popRoot,
|
onBack = ::popRoot,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
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.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -81,7 +81,7 @@ internal fun ScrcpyAllOptionsScreen(
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
Icons.AutoMirrored.Rounded.ArrowBack,
|
||||||
contentDescription = "返回"
|
contentDescription = "返回"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.core.net.toUri
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.BuildConfig
|
import io.github.miuzarte.scrcpyforandroid.BuildConfig
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
|
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||||
@@ -68,6 +67,7 @@ fun SettingsScreen(
|
|||||||
snackbar: SnackbarController,
|
snackbar: SnackbarController,
|
||||||
onOpenReorderDevices: () -> Unit,
|
onOpenReorderDevices: () -> Unit,
|
||||||
onOpenVirtualButtonOrder: () -> Unit,
|
onOpenVirtualButtonOrder: () -> Unit,
|
||||||
|
onOpenAbout: () -> Unit,
|
||||||
onPickServer: () -> Unit,
|
onPickServer: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -84,6 +84,7 @@ fun SettingsScreen(
|
|||||||
snackbar = snackbar,
|
snackbar = snackbar,
|
||||||
onOpenReorderDevices = onOpenReorderDevices,
|
onOpenReorderDevices = onOpenReorderDevices,
|
||||||
onOpenVirtualButtonOrder = onOpenVirtualButtonOrder,
|
onOpenVirtualButtonOrder = onOpenVirtualButtonOrder,
|
||||||
|
onOpenAbout = onOpenAbout,
|
||||||
onPickServer = onPickServer,
|
onPickServer = onPickServer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -96,6 +97,7 @@ fun SettingsPage(
|
|||||||
snackbar: SnackbarController,
|
snackbar: SnackbarController,
|
||||||
onOpenReorderDevices: () -> Unit,
|
onOpenReorderDevices: () -> Unit,
|
||||||
onOpenVirtualButtonOrder: () -> Unit,
|
onOpenVirtualButtonOrder: () -> Unit,
|
||||||
|
onOpenAbout: () -> Unit,
|
||||||
onPickServer: () -> Unit,
|
onPickServer: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -155,35 +157,23 @@ fun SettingsPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val updateSummary = remember(updateState) {
|
val updateSummary = remember(updateState) {
|
||||||
when (val state = updateState) {
|
"当前版本 ${BuildConfig.VERSION_NAME}" + when (val state = updateState) {
|
||||||
AppUpdateChecker.State.Idle ->
|
AppUpdateChecker.State.Idle -> ""
|
||||||
"当前版本 ${BuildConfig.VERSION_NAME}"
|
AppUpdateChecker.State.Checking -> ",正在检查更新"
|
||||||
|
AppUpdateChecker.State.Error -> ",检查更新失败"
|
||||||
|
|
||||||
AppUpdateChecker.State.Checking ->
|
is AppUpdateChecker.State.Ready -> when {
|
||||||
"当前版本 ${BuildConfig.VERSION_NAME},正在检查更新"
|
|
||||||
|
|
||||||
is AppUpdateChecker.State.Ready -> {
|
|
||||||
when {
|
|
||||||
state.release.hasUpdate ->
|
state.release.hasUpdate ->
|
||||||
"当前版本 ${state.release.currentVersion},发现新版本 ${state.release.latestVersion}"
|
",发现新版本 ${state.release.latestVersion}"
|
||||||
|
|
||||||
state.release.currentVersion == state.release.latestVersion.removePrefix("v")
|
state.release.currentVersion == state.release.latestVersion.removePrefix("v")
|
||||||
|| state.release.currentVersion == state.release.latestVersion ->
|
|| state.release.currentVersion == state.release.latestVersion ->
|
||||||
"当前版本 ${state.release.currentVersion},已是最新版本"
|
",已是最新版本"
|
||||||
|
|
||||||
else ->
|
else -> ",高于最新发布版本 ${state.release.latestVersion}"
|
||||||
"当前版本 ${state.release.currentVersion},高于最新发布版本 ${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(
|
LazyColumn(
|
||||||
@@ -191,7 +181,7 @@ fun SettingsPage(
|
|||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
SectionSmallTitle("主题", showLeadingSpacer = false)
|
SectionSmallTitle("主题")
|
||||||
Card {
|
Card {
|
||||||
OverlayDropdownPreference(
|
OverlayDropdownPreference(
|
||||||
title = "外观模式",
|
title = "外观模式",
|
||||||
@@ -281,7 +271,7 @@ fun SettingsPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SectionSmallTitle("scrcpy-server", showLeadingSpacer = false)
|
SectionSmallTitle("scrcpy-server")
|
||||||
Card {
|
Card {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = UiSpacing.Large),
|
modifier = Modifier.padding(vertical = UiSpacing.Large),
|
||||||
@@ -388,7 +378,7 @@ fun SettingsPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SectionSmallTitle("ADB", showLeadingSpacer = false)
|
SectionSmallTitle("ADB")
|
||||||
Card {
|
Card {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = UiSpacing.Large),
|
modifier = Modifier.padding(vertical = UiSpacing.Large),
|
||||||
@@ -446,7 +436,7 @@ fun SettingsPage(
|
|||||||
if (needMigration) item {
|
if (needMigration) item {
|
||||||
// 这部分应该不会显示出来,
|
// 这部分应该不会显示出来,
|
||||||
// 应用启动时就会执行迁移与旧数据的删除
|
// 应用启动时就会执行迁移与旧数据的删除
|
||||||
SectionSmallTitle("应用", showLeadingSpacer = false)
|
SectionSmallTitle("应用")
|
||||||
Card {
|
Card {
|
||||||
ArrowPreference(
|
ArrowPreference(
|
||||||
title = "恢复旧版本配置",
|
title = "恢复旧版本配置",
|
||||||
@@ -479,19 +469,12 @@ fun SettingsPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SectionSmallTitle("关于", showLeadingSpacer = false)
|
SectionSmallTitle("")
|
||||||
Card {
|
Card {
|
||||||
ArrowPreference(
|
ArrowPreference(
|
||||||
title = "仓库发布页",
|
title = "关于",
|
||||||
summary = "$updateSummary\n${AppUpdateChecker.REPO_URL.removePrefix("https://")}",
|
summary = updateSummary,
|
||||||
onClick = {
|
onClick = onOpenAbout,
|
||||||
context.startActivity(
|
|
||||||
Intent(
|
|
||||||
Intent.ACTION_VIEW,
|
|
||||||
updateUrl.toUri(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package io.github.miuzarte.scrcpyforandroid.pages
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.material.icons.Icons
|
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.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -48,7 +48,7 @@ internal fun VirtualButtonOrderScreen(
|
|||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
Icons.AutoMirrored.Rounded.ArrowBack,
|
||||||
contentDescription = "返回"
|
contentDescription = "返回"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ object AppUpdateChecker {
|
|||||||
sealed interface State {
|
sealed interface State {
|
||||||
data object Idle : State
|
data object Idle : State
|
||||||
data object Checking : State
|
data object Checking : State
|
||||||
|
data object Error : State
|
||||||
data class Ready(val release: ReleaseInfo) : State
|
data class Ready(val release: ReleaseInfo) : State
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ object AppUpdateChecker {
|
|||||||
Log.WARN,
|
Log.WARN,
|
||||||
error
|
error
|
||||||
)
|
)
|
||||||
_state.value = State.Idle
|
_state.value = State.Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.widgets
|
package io.github.miuzarte.scrcpyforandroid.widgets
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
|
||||||
import top.yukonga.miuix.kmp.basic.SmallTitle
|
import top.yukonga.miuix.kmp.basic.SmallTitle
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionSmallTitle(
|
fun SectionSmallTitle(
|
||||||
text: String,
|
text: String,
|
||||||
showLeadingSpacer: Boolean = true,
|
|
||||||
insideMargin: PaddingValues = PaddingValues(16.dp, 8.dp),
|
insideMargin: PaddingValues = PaddingValues(16.dp, 8.dp),
|
||||||
) {
|
) {
|
||||||
if (showLeadingSpacer) {
|
|
||||||
Spacer(Modifier.height(UiSpacing.SectionTitleLeadingGap))
|
|
||||||
}
|
|
||||||
SmallTitle(
|
SmallTitle(
|
||||||
text = text,
|
text = text,
|
||||||
insideMargin = insideMargin,
|
insideMargin = insideMargin,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.icons.Icons
|
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.VolumeDown
|
||||||
import androidx.compose.material.icons.automirrored.rounded.VolumeOff
|
import androidx.compose.material.icons.automirrored.rounded.VolumeOff
|
||||||
import androidx.compose.material.icons.automirrored.rounded.VolumeUp
|
import androidx.compose.material.icons.automirrored.rounded.VolumeUp
|
||||||
@@ -70,7 +70,7 @@ enum class VirtualButtonAction(
|
|||||||
BACK(
|
BACK(
|
||||||
"back",
|
"back",
|
||||||
"返回",
|
"返回",
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
Icons.AutoMirrored.Rounded.ArrowBack,
|
||||||
UiAndroidKeycodes.BACK
|
UiAndroidKeycodes.BACK
|
||||||
),
|
),
|
||||||
APP_SWITCH(
|
APP_SWITCH(
|
||||||
|
|||||||
@@ -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" }
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
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-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-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-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" }
|
miuix-navigation3-ui = { group = "top.yukonga.miuix.kmp", name = "miuix-navigation3-ui", version.ref = "miuix" }
|
||||||
|
|||||||
Reference in New Issue
Block a user