refactor: UI refresh

- 仍然是抄的 KernelSU 管理器和 miuix example
- TopAppBar 与 dock 栏模糊
- 液态玻璃(`com.kyant.backdrop`)悬浮 dock 栏
This commit is contained in:
Miuzarte
2026-04-14 16:54:04 +08:00
parent 89c111e23b
commit 0f4a82d8d5
41 changed files with 2340 additions and 661 deletions

View File

@@ -40,8 +40,8 @@ android {
applicationId = "io.github.miuzarte.scrcpyforandroid"
minSdk = 26
targetSdk = 36
versionCode = 11
versionName = "0.1.5"
versionCode = 12
versionName = "0.1.6"
externalNativeBuild {
cmake {
@@ -114,6 +114,7 @@ dependencies {
implementation(libs.miuix.preference)
implementation(libs.miuix.icons)
implementation(libs.miuix.navigation3.ui)
implementation(libs.backdrop)
implementation("io.github.vvb2060.ndk:boringssl:20250114")
implementation("org.lsposed.libcxx:libcxx:27.0.12077973")
implementation("org.bouncycastle:bcpkix-jdk18on:1.80")

View File

@@ -36,6 +36,7 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -55,6 +56,7 @@ import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
import io.github.miuzarte.scrcpyforandroid.services.SnackbarController
import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -80,6 +82,7 @@ import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.blur.layerBackdrop
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Close
import top.yukonga.miuix.kmp.icon.extended.Ok
@@ -90,6 +93,8 @@ import top.yukonga.miuix.kmp.preference.ArrowPreference
import top.yukonga.miuix.kmp.preference.SwitchPreference
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
import top.yukonga.miuix.kmp.theme.ThemeController
class LockscreenPasswordActivity : FragmentActivity() {
@@ -114,7 +119,10 @@ class LockscreenPasswordActivity : FragmentActivity() {
remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
val haptics = rememberAppHaptics()
MiuixTheme(controller = themeController) {
MiuixTheme(
controller = themeController,
smoothRounding = asBundle.smoothCorner,
) {
CompositionLocalProvider(
LocalSnackbarController provides snackbarController,
LocalAppHaptics provides haptics,
@@ -213,10 +221,18 @@ private fun LockscreenPasswordScreen(
pendingCreate = false
}
val blurBackdrop = rememberBlurBackdrop(asBundle.blur)
Scaffold(
topBar = {
TopAppBar(
title = "锁屏密码自动填充",
modifier =
if (blurBackdrop != null) Modifier.layerBackdrop(blurBackdrop)
else Modifier,
color =
if (blurBackdrop != null) Color.Transparent
else colorScheme.surface,
navigationIcon = {
IconButton(onClick = { activity.onBackPressedDispatcher.onBackPressed() }) {
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "返回")
@@ -442,6 +458,7 @@ private fun LockscreenPasswordPage(
LazyColumn(
contentPadding = contentPadding,
scrollBehavior = scrollBehavior,
bottomInnerPadding = UiSpacing.PageBottom,
) {
item {
Card {
@@ -514,16 +531,14 @@ private fun LockscreenPasswordPage(
"\n2. 在 root / posed / hook / 调试器 / 恶意输入法 等环境下,密码仍可能泄露" +
"\n3. 本功能不会绕过系统锁屏认证,仅用于你已合法授权控制的设备" +
"\n4. 关闭“填充密码时需要验证”会显著降低安全性,请谨慎选择",
fontSize = MiuixTheme.textStyles.body2.fontSize,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
fontSize = textStyles.body2.fontSize,
color = colorScheme.onSurfaceVariantSummary,
modifier = Modifier
.fillMaxWidth()
.padding(top = UiSpacing.Large)
.padding(horizontal = UiSpacing.Large),
)
}
item { Spacer(Modifier.height(UiSpacing.PageBottom)) }
}
}
@@ -538,7 +553,6 @@ private fun PasswordMenuPopup(
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
alignment = PopupPositionProvider.Align.TopEnd,
onDismissRequest = onDismissRequest,
enableWindowDim = false,
) {
ListPopupColumn {
SpinnerItemImpl(

View File

@@ -1,10 +1,9 @@
package io.github.miuzarte.scrcpyforandroid
import android.os.Build
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.fragment.app.FragmentActivity
import io.github.miuzarte.scrcpyforandroid.pages.MainScreen
import io.github.miuzarte.scrcpyforandroid.password.BiometricGate
@@ -13,12 +12,10 @@ import io.github.miuzarte.scrcpyforandroid.password.hasAuthenticatedOrigin
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
import io.github.miuzarte.scrcpyforandroid.services.AppWakeLocks
import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration
import io.github.miuzarte.scrcpyforandroid.storage.Storage
import kotlinx.coroutines.runBlocking
// 生物认证需要 FragmentActivity
class MainActivity : FragmentActivity() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@@ -110,7 +110,6 @@ class StreamActivity : FragmentActivity() {
//+ onPictureInPictureUiStateChanged
//- onUserLeaveHint
// @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
super.onPictureInPictureUiStateChanged(pipState)

View File

@@ -1,10 +1,9 @@
package io.github.miuzarte.scrcpyforandroid.nativecore
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import java.io.IOException
import java.net.InetSocketAddress
import java.net.NetworkInterface
@@ -15,7 +14,6 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
@RequiresApi(Build.VERSION_CODES.R)
/**
* Performs mDNS discovery for ADB TLS pairing/connect services on the local network.
*
@@ -26,7 +24,7 @@ internal object AdbMdnsDiscoverer {
private lateinit var nsdManager: NsdManager
fun init(context: android.content.Context) {
fun init(context: Context) {
if (::nsdManager.isInitialized) return
nsdManager = context.applicationContext.getSystemService(NsdManager::class.java)
}

View File

@@ -1,8 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.nativecore
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import org.conscrypt.Conscrypt
import java.io.Closeable
import java.io.DataInputStream
@@ -131,7 +129,6 @@ private class PairingContext private constructor(private val nativePtr: Long) {
* keying material using the user-supplied pairing code, and exchanges peer
* information to complete pairing.
*/
@RequiresApi(Build.VERSION_CODES.R)
internal class DirectAdbPairingClient(
private val host: String,
private val port: Int,

View File

@@ -1,10 +1,9 @@
package io.github.miuzarte.scrcpyforandroid.pages
import android.content.Intent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectTapGestures
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
@@ -13,6 +12,7 @@ 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.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
@@ -23,15 +23,17 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
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.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
@@ -45,7 +47,10 @@ 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.pages.effect.BgEffectBackground
import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import kotlinx.coroutines.flow.onEach
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults
@@ -60,22 +65,20 @@ 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
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
@Composable
internal fun AboutScreen() {
val navigator = LocalRootNavigator.current
val enableBlur = LocalEnableBlur.current
val blurBackdrop = rememberBlurBackdrop(enableBlur)
val topAppBarScrollBehavior = MiuixScrollBehavior()
val lazyListState = rememberLazyListState()
var logoHeightPx by remember { mutableIntStateOf(0) }
@@ -101,7 +104,12 @@ internal fun AboutScreen() {
SmallTopAppBar(
title = "关于",
scrollBehavior = topAppBarScrollBehavior,
color = colorScheme.surface.copy(alpha = if (scrollProgress == 1f) 1f else 0f),
modifier =
if (blurBackdrop != null) Modifier.layerBackdrop(blurBackdrop)
else Modifier,
color =
if (blurBackdrop != null) Color.Transparent
else colorScheme.surface.copy(alpha = if (scrollProgress == 1f) 1f else 0f),
titleColor = colorScheme.onSurface.copy(alpha = scrollProgress),
defaultWindowInsetsPadding = false,
navigationIcon = {
@@ -120,6 +128,7 @@ internal fun AboutScreen() {
top = innerPadding.calculateTopPadding(),
bottom = innerPadding.calculateBottomPadding(),
),
enableBlur = enableBlur,
lazyListState = lazyListState,
scrollProgress = scrollProgress,
onLogoHeightChanged = { logoHeightPx = it },
@@ -130,16 +139,20 @@ internal fun AboutScreen() {
@Composable
private fun AboutContent(
padding: PaddingValues,
lazyListState: androidx.compose.foundation.lazy.LazyListState,
enableBlur: Boolean,
lazyListState: 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
var isOs3Effect by remember { mutableStateOf(true) }
val blurEnabled = remember(enableBlur) { enableBlur && isRenderEffectSupported() }
val isDark = colorScheme.background.luminance() < 0.5f
val heroBlendColors = remember(isDark) { heroBlendColors(isDark) }
val cardBlendColors = remember(isDark) { aboutCardBlendToken(isDark) }
var logoAreaY by remember { mutableFloatStateOf(0f) }
var projectNameY by remember { mutableFloatStateOf(0f) }
var versionCodeY by remember { mutableFloatStateOf(0f) }
@@ -172,12 +185,12 @@ private fun AboutContent(
val stage2TotalLength = versionCodeY - projectNameY
val versionCodeDelay = stage1TotalLength * 0.5f
versionCodeProgress =
((offset.toFloat() - versionCodeDelay) / (stage1TotalLength - versionCodeDelay).coerceAtLeast(
1f
))
((offset.toFloat() - versionCodeDelay) / (stage1TotalLength - versionCodeDelay)
.coerceAtLeast(1f))
.coerceIn(0f, 1f)
projectNameProgress =
((offset.toFloat() - stage1TotalLength) / stage2TotalLength.coerceAtLeast(1f))
((offset.toFloat() - stage1TotalLength) / stage2TotalLength
.coerceAtLeast(1f))
.coerceIn(0f, 1f)
}
.collect {}
@@ -186,8 +199,11 @@ private fun AboutContent(
BgEffectBackground(
dynamicBackground = true,
modifier = Modifier.fillMaxSize(),
bgModifier = Modifier.layerBackdrop(backdrop),
bgModifier =
if (blurEnabled) Modifier.layerBackdrop(backdrop)
else Modifier,
effectBackground = true,
isOs3Effect = isOs3Effect,
alpha = { 1f - scrollProgress },
) {
Column(
@@ -206,12 +222,12 @@ private fun AboutContent(
) {
val density = LocalDensity.current
val textMeasurer = rememberTextMeasurer()
val baseTitleFontSize = 35.sp
val baseTitleFontSize = 32.sp
val titleLayout = remember(textMeasurer) {
textMeasurer.measure(
text = "Scrcpy for Android",
style = TextStyle(
fontWeight = FontWeight.Bold,
fontWeight = FontWeight.Black,
fontSize = baseTitleFontSize,
),
softWrap = false,
@@ -226,7 +242,7 @@ private fun AboutContent(
Text(
text = "Scrcpy for Android",
modifier = Modifier
.padding(top = 12.dp, bottom = 5.dp)
.padding(top = 12.dp, bottom = 6.dp)
.onGloballyPositioned { coordinates ->
if (projectNameY != 0f) return@onGloballyPositioned
val y = coordinates.positionInWindow().y
@@ -243,13 +259,11 @@ private fun AboutContent(
shape = SmoothRoundedCornerShape(16.dp),
blurRadius = 150f,
noiseCoefficient = BlurDefaults.NoiseCoefficient,
colors = BlurColors(
blendColors = heroBlendColors(isDark),
),
contentBlendMode = ComposeBlendMode.DstIn,
colors = BlurColors(blendColors = heroBlendColors),
contentBlendMode = BlendMode.DstIn,
enabled = blurEnabled,
),
color = MiuixTheme.colorScheme.onBackground,
color = colorScheme.onBackground,
fontWeight = FontWeight.Bold,
fontSize = titleFontSize,
)
@@ -269,7 +283,7 @@ private fun AboutContent(
val size = coordinates.size
versionCodeY = y + size.height
},
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
color = colorScheme.onSurfaceVariantSummary,
fontSize = 14.sp,
textAlign = TextAlign.Center,
)
@@ -288,7 +302,7 @@ private fun AboutContent(
val size = coordinates.size
versionCodeY = y + size.height
},
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
color = colorScheme.onSurfaceVariantSummary,
fontSize = 14.sp,
textAlign = TextAlign.Center,
)
@@ -309,6 +323,13 @@ private fun AboutContent(
.fillMaxWidth()
.height(320.dp)
.padding(top = 36.dp)
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
isOs3Effect = !isOs3Effect
}
)
}
.onSizeChanged { onLogoHeightChanged(it.height) }
.onGloballyPositioned { coordinates ->
val y = coordinates.positionInWindow().y
@@ -336,7 +357,7 @@ private fun AboutContent(
endActions = {
Text(
text = "GitHub",
fontSize = MiuixTheme.textStyles.body2.fontSize,
fontSize = textStyles.body2.fontSize,
color = colorScheme.onSurfaceVariantActions,
)
},
@@ -352,7 +373,7 @@ private fun AboutContent(
releaseStatusText?.let {
Text(
text = it,
fontSize = MiuixTheme.textStyles.body2.fontSize,
fontSize = textStyles.body2.fontSize,
color = colorScheme.onSurfaceVariantActions,
)
}
@@ -375,7 +396,7 @@ private fun AboutContent(
endActions = {
Text(
text = "Apache-2.0",
fontSize = MiuixTheme.textStyles.body2.fontSize,
fontSize = textStyles.body2.fontSize,
color = colorScheme.onSurfaceVariantActions,
)
},
@@ -413,335 +434,35 @@ private fun AboutCard(
enabled = blurEnabled,
),
colors = CardDefaults.defaultColors(
if (blurEnabled) Color.Transparent else MiuixTheme.colorScheme.surfaceContainer,
Color.Transparent,
color =
if (blurEnabled) Color.Transparent
else colorScheme.surfaceContainer,
contentColor = 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),
)
}
}
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),
)
}
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

@@ -6,6 +6,7 @@ import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -40,6 +41,7 @@ import androidx.compose.ui.graphics.BlendModeColorFilter
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
@@ -70,6 +72,9 @@ import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyProfiles
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTileList
import io.github.miuzarte.scrcpyforandroid.widgets.PairingCard
@@ -102,12 +107,14 @@ import top.yukonga.miuix.kmp.basic.SpinnerEntry
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.blur.layerBackdrop
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.basic.Check
import top.yukonga.miuix.kmp.icon.extended.Store
import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet
import top.yukonga.miuix.kmp.overlay.OverlayListPopup
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
private const val ADB_CONNECT_TIMEOUT_MS = 12_000L
private const val ADB_KEEPALIVE_INTERVAL_MS = 3_000L
@@ -129,52 +136,63 @@ private data class StartAppRequest(
fun DeviceTabScreen(
scrollBehavior: ScrollBehavior,
scrcpy: Scrcpy,
bottomInnerPadding: Dp,
onOpenReorderDevices: () -> Unit,
) {
val navigator = LocalRootNavigator.current
var showThreePointMenu by rememberSaveable { mutableStateOf(false) }
val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current)
val blurActive = blurBackdrop != null
Scaffold(
topBar = {
TopAppBar(
title = "设备",
actions = {
IconButton(
onClick = { showThreePointMenu = true },
holdDownState = showThreePointMenu,
) {
Icon(
Icons.Rounded.MoreVert,
contentDescription = "更多"
BlurredBar(backdrop = blurBackdrop) {
TopAppBar(
title = "设备",
color =
if (blurActive) Color.Transparent
else colorScheme.surface,
actions = {
IconButton(
onClick = { showThreePointMenu = true },
holdDownState = showThreePointMenu,
) {
Icon(
Icons.Rounded.MoreVert,
contentDescription = "更多"
)
}
DeviceMenuPopup(
show = showThreePointMenu,
onDismissRequest = { showThreePointMenu = false },
onReorderDevices = {
onOpenReorderDevices()
showThreePointMenu = false
},
onOpenVirtualButtonOrder = {
navigator.push(RootScreen.VirtualButtonOrder)
showThreePointMenu = false
},
canClearLogs = EventLogger.hasLogs(),
onClearLogs = {
EventLogger.clearLogs()
showThreePointMenu = false
},
)
}
DeviceMenuPopup(
show = showThreePointMenu,
onDismissRequest = { showThreePointMenu = false },
onReorderDevices = {
onOpenReorderDevices()
showThreePointMenu = false
},
onOpenVirtualButtonOrder = {
navigator.push(RootScreen.VirtualButtonOrder)
showThreePointMenu = false
},
canClearLogs = EventLogger.hasLogs(),
onClearLogs = {
EventLogger.clearLogs()
showThreePointMenu = false
},
)
},
scrollBehavior = scrollBehavior,
)
},
scrollBehavior = scrollBehavior,
)
}
},
) { pagePadding ->
DeviceTabPage(
contentPadding = pagePadding,
scrollBehavior = scrollBehavior,
scrcpy = scrcpy,
)
Box(modifier = if (blurActive) Modifier.layerBackdrop(blurBackdrop) else Modifier) {
DeviceTabPage(
contentPadding = pagePadding,
scrollBehavior = scrollBehavior,
scrcpy = scrcpy,
bottomInnerPadding = bottomInnerPadding,
)
}
}
}
@@ -183,6 +201,7 @@ fun DeviceTabPage(
contentPadding: PaddingValues,
scrollBehavior: ScrollBehavior,
scrcpy: Scrcpy,
bottomInnerPadding: Dp,
) {
val activity = LocalActivity.current
val fragmentActivity = remember(activity) { activity as? FragmentActivity }
@@ -943,6 +962,7 @@ fun DeviceTabPage(
contentPadding = contentPadding,
scrollBehavior = scrollBehavior,
state = listState,
bottomInnerPadding = bottomInnerPadding,
) {
item {
StatusCard(
@@ -1073,8 +1093,8 @@ fun DeviceTabPage(
recentTasks.isNotEmpty() -> recentTasks.size.toString()
else -> ""
},
color = MiuixTheme.colorScheme.onSurfaceVariantActions,
fontSize = MiuixTheme.textStyles.body2.fontSize,
color = colorScheme.onSurfaceVariantActions,
fontSize = textStyles.body2.fontSize,
modifier = Modifier.padding(end = UiSpacing.ContentVertical),
)
},
@@ -1180,7 +1200,6 @@ fun DeviceTabPage(
}
}
item { Spacer(Modifier.height(UiSpacing.PageBottom)) }
}
RecentTasksBottomSheet(
@@ -1224,7 +1243,6 @@ private fun DeviceMenuPopup(
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
alignment = PopupPositionProvider.Align.TopEnd,
onDismissRequest = onDismissRequest,
enableWindowDim = false,
) {
ListPopupColumn {
DeviceMenuPopupItem(
@@ -1376,7 +1394,7 @@ private fun RecentTaskSheetItem(
entry.title?.let {
Text(
text = it,
fontSize = MiuixTheme.textStyles.body1.fontSize,
fontSize = textStyles.body1.fontSize,
fontWeight = FontWeight.Medium,
color = spinnerColors.contentColor,
)
@@ -1384,7 +1402,7 @@ private fun RecentTaskSheetItem(
entry.summary?.let {
Text(
text = it,
fontSize = MiuixTheme.textStyles.body2.fontSize,
fontSize = textStyles.body2.fontSize,
color = spinnerColors.summaryColor,
)
}
@@ -1425,9 +1443,9 @@ private fun DeviceMenuPopupItem(
if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem
Text(
text = text,
fontSize = MiuixTheme.textStyles.body1.fontSize,
fontSize = textStyles.body1.fontSize,
fontWeight = FontWeight.Medium,
color = MiuixTheme.colorScheme.disabledOnSecondaryVariant,
color = colorScheme.disabledOnSecondaryVariant,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = UiSpacing.PopupHorizontal)

View File

@@ -1,7 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.pages
import android.annotation.SuppressLint
import android.app.Activity
import android.graphics.Rect
import android.util.Log
import androidx.activity.compose.BackHandler
@@ -38,7 +37,6 @@ import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.sp

View File

@@ -10,6 +10,10 @@ import androidx.activity.compose.PredictiveBackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
@@ -32,13 +36,21 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.ui.NavDisplay
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import io.github.miuzarte.scrcpyforandroid.BuildConfig
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
@@ -55,6 +67,13 @@ import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBar
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBarBlur
import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBar
import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBarItem
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -62,15 +81,19 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.NavigationBar
import top.yukonga.miuix.kmp.basic.NavigationBarItem
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.SnackbarHost
import top.yukonga.miuix.kmp.basic.SnackbarHostState
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.theme.ThemeController
import top.yukonga.miuix.kmp.blur.layerBackdrop as miuixLayerBackdrop
private enum class MainBottomTabDestination(
val label: String,
@@ -316,59 +339,151 @@ fun MainScreen() {
val rootEntryProvider = entryProvider<NavKey> {
entry(RootScreen.Home) {
val blurBackdrop = rememberBlurBackdrop(enableBlur = asBundle.blur)
val floatingBarBlurActive = asBundle.blur && asBundle.floatingBottomBarBlur
val surfaceColor = colorScheme.surface
val glassBackdrop = rememberLayerBackdrop {
drawRect(surfaceColor)
drawContent()
}
fun navigateToTab(tab: MainBottomTabDestination) {
scope.launch {
pagerState.animateScrollToPage(
page = tab.ordinal,
animationSpec = spring(
dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
stiffness = UiMotion.PAGE_SWITCH_STIFFNESS,
),
)
}
}
Scaffold(
bottomBar = {
NavigationBar {
tabs.forEach { tab ->
NavigationBarItem(
selected = currentTab == tab,
onClick = {
scope.launch {
pagerState.animateScrollToPage(
page = tab.ordinal,
animationSpec = spring(
dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
stiffness = UiMotion.PAGE_SWITCH_STIFFNESS,
),
)
}
},
icon = tab.icon,
label = tab.label,
)
if (!asBundle.floatingBottomBar) {
BlurredBar(backdrop = blurBackdrop) {
NavigationBar(
color =
if (blurBackdrop != null) Color.Transparent
else colorScheme.surface,
) {
tabs.forEach { tab ->
NavigationBarItem(
selected = currentTab == tab,
onClick = { navigateToTab(tab) },
icon = tab.icon,
label = tab.label,
)
}
}
}
}
},
snackbarHost = { SnackbarHost(snackHostState) },
) { contentPadding ->
HorizontalPager(
modifier = Modifier
.fillMaxSize()
.padding(bottom = contentPadding.calculateBottomPadding()),
state = pagerState,
beyondViewportPageCount = 1,
) { page ->
val tab = tabs[page]
saveableStateHolder.SaveableStateProvider(tab.name) {
when (tab) {
MainBottomTabDestination.Device -> DeviceTabScreen(
scrollBehavior = devicesPageScrollBehavior,
scrcpy = scrcpy,
onOpenReorderDevices = { showReorderDevices = true },
)
MainBottomTabDestination.Settings -> SettingsScreen(
scrollBehavior = settingsPageScrollBehavior,
onOpenReorderDevices = { showReorderDevices = true },
)
}
}
val bottomInnerPadding: Dp = if (asBundle.floatingBottomBar) {
12.dp + 64.dp + contentPadding.calculateBottomPadding()
} else {
contentPadding.calculateBottomPadding()
}
ReorderDevicesScreen(
show = showReorderDevices,
onDismissRequest = { showReorderDevices = false },
)
Box(
modifier = Modifier.fillMaxSize(),
) {
Box(
modifier = Modifier
.fillMaxSize()
.then(if (blurBackdrop != null) Modifier.miuixLayerBackdrop(blurBackdrop) else Modifier),
) {
HorizontalPager(
modifier = Modifier
.fillMaxSize()
.then(
if (asBundle.floatingBottomBar && floatingBarBlurActive) {
Modifier.layerBackdrop(glassBackdrop)
} else {
Modifier
}
),
state = pagerState,
beyondViewportPageCount = 1,
) { page ->
val tab = tabs[page]
saveableStateHolder.SaveableStateProvider(tab.name) {
when (tab) {
MainBottomTabDestination.Device -> DeviceTabScreen(
scrollBehavior = devicesPageScrollBehavior,
scrcpy = scrcpy,
bottomInnerPadding = bottomInnerPadding,
onOpenReorderDevices = { showReorderDevices = true },
)
MainBottomTabDestination.Settings -> SettingsScreen(
scrollBehavior = settingsPageScrollBehavior,
bottomInnerPadding = bottomInnerPadding,
onOpenReorderDevices = { showReorderDevices = true },
)
}
}
}
}
if (asBundle.floatingBottomBar) {
FloatingBottomBar(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 12.dp + contentPadding.calculateBottomPadding())
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {},
),
selectedIndex = { pagerState.currentPage },
onSelected = { index ->
scope.launch {
pagerState.animateScrollToPage(
page = index,
animationSpec = spring(
dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
stiffness = UiMotion.PAGE_SWITCH_STIFFNESS,
),
)
}
},
backdrop = glassBackdrop,
tabsCount = tabs.size,
isBlurEnabled = floatingBarBlurActive,
) {
tabs.forEach { tab ->
FloatingBottomBarItem(
onClick = { navigateToTab(tab) },
modifier = Modifier.defaultMinSize(minWidth = 76.dp),
) {
Icon(
imageVector = tab.icon,
contentDescription = tab.label,
tint = colorScheme.onSurface,
)
Text(
text = tab.label,
fontSize = 11.sp,
lineHeight = 14.sp,
color = colorScheme.onSurface,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Visible,
)
}
}
}
}
ReorderDevicesScreen(
show = showReorderDevices,
onDismissRequest = { showReorderDevices = false },
)
}
}
}
@@ -403,8 +518,14 @@ fun MainScreen() {
}
val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
MiuixTheme(controller = themeController) {
MiuixTheme(
controller = themeController,
smoothRounding = asBundle.smoothCorner,
) {
CompositionLocalProvider(
LocalEnableBlur provides asBundle.blur,
LocalEnableFloatingBottomBar provides asBundle.floatingBottomBar,
LocalEnableFloatingBottomBarBlur provides asBundle.floatingBottomBarBlur,
LocalRootNavigator provides rootNavigator,
LocalSnackbarController provides snackbarController,
LocalAppHaptics provides haptics,

View File

@@ -3,6 +3,7 @@ package io.github.miuzarte.scrcpyforandroid.pages
import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -34,6 +35,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
@@ -63,6 +65,9 @@ import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyProfiles
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -87,6 +92,7 @@ import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.blur.layerBackdrop
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Store
import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet
@@ -96,7 +102,7 @@ import top.yukonga.miuix.kmp.preference.ArrowPreference
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
import top.yukonga.miuix.kmp.preference.OverlaySpinnerPreference
import top.yukonga.miuix.kmp.preference.SwitchPreference
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import kotlin.math.roundToInt
@Composable
@@ -105,6 +111,8 @@ internal fun ScrcpyAllOptionsScreen(
scrcpy: Scrcpy,
) {
val navigator = LocalRootNavigator.current
val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current)
val blurActive = blurBackdrop != null
val scope = rememberCoroutineScope()
val snackbar = LocalSnackbarController.current
var showProfileMenu by rememberSaveable { mutableStateOf(false) }
@@ -211,81 +219,92 @@ internal fun ScrcpyAllOptionsScreen(
Scaffold(
topBar = {
TopAppBar(
title = "所有参数",
navigationIcon = {
IconButton(onClick = navigator.pop) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = "返回"
BlurredBar(backdrop = blurBackdrop) {
TopAppBar(
title = "所有参数",
color =
if (blurActive) Color.Transparent
else colorScheme.surface,
navigationIcon = {
IconButton(onClick = navigator.pop) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = "返回"
)
}
},
actions = {
IconButton(
onClick = { showProfileMenu = true },
holdDownState = showProfileMenu,
) {
Icon(
Icons.Rounded.MoreVert,
contentDescription = "配置管理",
)
}
ProfileMenuPopup(
show = showProfileMenu,
onDismissRequest = { showProfileMenu = false },
onManageProfiles = {
showManageProfilesSheet = true
showProfileMenu = false
},
)
}
},
actions = {
IconButton(
onClick = { showProfileMenu = true },
holdDownState = showProfileMenu,
) {
Icon(
Icons.Rounded.MoreVert,
contentDescription = "配置管理",
)
}
ProfileMenuPopup(
show = showProfileMenu,
onDismissRequest = { showProfileMenu = false },
onManageProfiles = {
showManageProfilesSheet = true
showProfileMenu = false
},
)
},
scrollBehavior = scrollBehavior,
bottomContent = {
TabRow(
tabs = profileTabs,
selectedTabIndex = selectedProfileIndex,
onTabSelected = { index ->
val nextProfileId = profileIds.getOrNull(index)
?: return@TabRow
if (nextProfileId == selectedProfileId) return@TabRow
scope.launch {
saveBundleForProfile(selectedProfileId, soBundleState.value)
bindCurrentConnectedDevice(nextProfileId)
selectedProfileId = nextProfileId
val profileName = profileTabs.getOrElse(index) { "全局" }
currentConnectedDeviceName?.let { deviceName ->
snackbar.show("$deviceName 已切换到配置 $profileName")
},
scrollBehavior = scrollBehavior,
bottomContent = {
TabRow(
tabs = profileTabs,
selectedTabIndex = selectedProfileIndex,
onTabSelected = { index ->
val nextProfileId = profileIds.getOrNull(index)
?: return@TabRow
if (nextProfileId == selectedProfileId) return@TabRow
scope.launch {
saveBundleForProfile(selectedProfileId, soBundleState.value)
bindCurrentConnectedDevice(nextProfileId)
selectedProfileId = nextProfileId
val profileName = profileTabs.getOrElse(index) { "全局" }
currentConnectedDeviceName?.let { deviceName ->
snackbar.show("$deviceName 已切换到配置 $profileName")
}
}
}
},
modifier = Modifier
.padding(bottom = UiSpacing.Medium)
.padding(horizontal = UiSpacing.Medium),
minWidth = 96.dp,
maxWidth = 192.dp,
height = 48.dp,
itemSpacing = UiSpacing.Medium,
)
}
)
},
modifier = Modifier
.padding(bottom = UiSpacing.Medium)
.padding(horizontal = UiSpacing.Medium),
minWidth = 96.dp,
maxWidth = 192.dp,
height = 48.dp,
itemSpacing = UiSpacing.Medium,
)
}
)
}
},
snackbarHost = {
val snackbar = LocalSnackbarController.current
SnackbarHost(snackbar.hostState)
},
) { contentPadding ->
ScrcpyAllOptionsPage(
contentPadding = contentPadding,
scrollBehavior = scrollBehavior,
scrcpy = scrcpy,
soBundleShared = soBundleShared,
scrcpyProfilesState = scrcpyProfilesState,
selectedProfileIdState = selectedProfileIdState,
soBundleState = soBundleState,
lastValidSoBundleState = lastValidSoBundleState,
onSaveBundleForProfile = ::saveBundleForProfile,
)
Box(
modifier =
if (blurActive) Modifier.layerBackdrop(blurBackdrop)
else Modifier,
) {
ScrcpyAllOptionsPage(
contentPadding = contentPadding,
scrollBehavior = scrollBehavior,
scrcpy = scrcpy,
soBundleShared = soBundleShared,
scrcpyProfilesState = scrcpyProfilesState,
selectedProfileIdState = selectedProfileIdState,
soBundleState = soBundleState,
lastValidSoBundleState = lastValidSoBundleState,
onSaveBundleForProfile = ::saveBundleForProfile,
)
}
ProfileNameDialog(
mode = activeProfileDialog,
@@ -752,6 +771,7 @@ internal fun ScrcpyAllOptionsPage(
LazyColumn(
contentPadding = contentPadding,
scrollBehavior = scrollBehavior,
bottomInnerPadding = UiSpacing.PageBottom,
) {
item {
Card {
@@ -1854,7 +1874,6 @@ internal fun ScrcpyAllOptionsPage(
}
}
item { Spacer(Modifier.height(UiSpacing.PageBottom)) }
}
}
@@ -1874,7 +1893,6 @@ private fun ProfileMenuPopup(
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
alignment = PopupPositionProvider.Align.TopEnd,
onDismissRequest = onDismissRequest,
enableWindowDim = false,
) {
ListPopupColumn {
ProfileMenuPopupItem(
@@ -1915,7 +1933,7 @@ private fun ProfileMenuPopupItem(
top = if (index == 0) UiSpacing.PopupHorizontal else UiSpacing.PageItem,
bottom = if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem,
),
color = MiuixTheme.colorScheme.disabledOnSecondaryVariant,
color = colorScheme.disabledOnSecondaryVariant,
fontWeight = FontWeight.Medium,
)
}

View File

@@ -4,12 +4,11 @@ import android.content.Intent
import android.os.Process
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Clear
@@ -26,8 +25,10 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import io.github.miuzarte.scrcpyforandroid.BuildConfig
import io.github.miuzarte.scrcpyforandroid.LockscreenPasswordActivity
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
@@ -42,6 +43,9 @@ import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration
import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -56,30 +60,48 @@ import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.blur.layerBackdrop
import top.yukonga.miuix.kmp.preference.ArrowPreference
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
import top.yukonga.miuix.kmp.preference.SwitchPreference
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import kotlin.math.roundToInt
import kotlin.system.exitProcess
@Composable
fun SettingsScreen(
scrollBehavior: ScrollBehavior,
bottomInnerPadding: Dp,
onOpenReorderDevices: () -> Unit,
) {
val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current)
val blurActive = blurBackdrop != null
Scaffold(
topBar = {
TopAppBar(
title = "设置",
scrollBehavior = scrollBehavior,
)
BlurredBar(backdrop = blurBackdrop) {
TopAppBar(
title = "设置",
color =
if (blurActive) Color.Transparent
else colorScheme.surface,
scrollBehavior = scrollBehavior,
)
}
},
) { pagePadding ->
SettingsPage(
contentPadding = pagePadding,
scrollBehavior = scrollBehavior,
onOpenReorderDevices = onOpenReorderDevices,
)
Box(
modifier =
if (blurActive) Modifier.layerBackdrop(blurBackdrop)
else Modifier,
) {
SettingsPage(
contentPadding = pagePadding,
scrollBehavior = scrollBehavior,
bottomInnerPadding = bottomInnerPadding,
onOpenReorderDevices = onOpenReorderDevices,
)
}
}
}
@@ -87,6 +109,7 @@ fun SettingsScreen(
fun SettingsPage(
contentPadding: PaddingValues,
scrollBehavior: ScrollBehavior,
bottomInnerPadding: Dp,
onOpenReorderDevices: () -> Unit,
) {
val context = LocalContext.current
@@ -172,6 +195,7 @@ fun SettingsPage(
LazyColumn(
contentPadding = contentPadding,
scrollBehavior = scrollBehavior,
bottomInnerPadding = bottomInnerPadding,
) {
item {
SectionSmallTitle("主题")
@@ -180,30 +204,60 @@ fun SettingsPage(
title = "外观模式",
summary = ThemeModes.baseOptions
.getOrNull(
asBundle.themeBaseIndex.coerceIn(
0,
ThemeModes.baseOptions.lastIndex
)
asBundle.themeBaseIndex
.coerceIn(0, ThemeModes.baseOptions.lastIndex)
)
?.label
?: "跟随系统",
items = themeItems,
selectedIndex = asBundle.themeBaseIndex.coerceIn(
0,
ThemeModes.baseOptions.lastIndex
),
selectedIndex = asBundle.themeBaseIndex
.coerceIn(0, ThemeModes.baseOptions.lastIndex),
onSelectedIndexChange = {
asBundle = asBundle.copy(themeBaseIndex = it)
},
)
SwitchPreference(
title = "Monet",
title = "Monet 颜色",
summary = "开启后使用 Monet 动态配色",
checked = asBundle.monet,
onCheckedChange = {
asBundle = asBundle.copy(monet = it)
},
)
SwitchPreference(
title = "模糊",
summary = "启用顶栏和底栏的模糊效果",
checked = asBundle.blur,
onCheckedChange = {
asBundle = asBundle.copy(blur = it)
}
)
SwitchPreference(
title = "悬浮底栏",
summary = "使用 Apple 风格的悬浮底栏",
checked = asBundle.floatingBottomBar,
onCheckedChange = {
asBundle = asBundle.copy(floatingBottomBar = it)
}
)
AnimatedVisibility(asBundle.floatingBottomBar) {
SwitchPreference(
title = "液态玻璃",
summary = "启用悬浮底栏的液态玻璃效果",
checked = asBundle.floatingBottomBarBlur,
onCheckedChange = {
asBundle = asBundle.copy(floatingBottomBarBlur = it)
}
)
}
SwitchPreference(
title = "平滑圆角",
summary = "启用全局平滑圆角效果",
checked = asBundle.smoothCorner,
onCheckedChange = {
asBundle = asBundle.copy(smoothCorner = it)
}
)
}
}
@@ -500,6 +554,5 @@ fun SettingsPage(
}
}
item { Spacer(Modifier.height(UiSpacing.PageBottom)) }
}
}

View File

@@ -99,7 +99,10 @@ fun StreamScreen(activity: StreamActivity) {
SnackbarController(scope = snackbarScope, hostState = snackbarHostState)
}
MiuixTheme(controller = themeController) {
MiuixTheme(
controller = themeController,
smoothRounding = asBundle.smoothCorner,
) {
CompositionLocalProvider(
LocalSnackbarController provides snackbarController,
LocalAppHaptics provides haptics,

View File

@@ -1,5 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.pages
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
@@ -12,16 +13,19 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
@@ -36,34 +40,45 @@ import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.blur.layerBackdrop
import top.yukonga.miuix.kmp.preference.SwitchPreference
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
@Composable
internal fun VirtualButtonOrderScreen(
scrollBehavior: ScrollBehavior,
) {
val navigator = LocalRootNavigator.current
val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current)
val blurActive = blurBackdrop != null
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
TopAppBar(
title = "虚拟按钮排序",
navigationIcon = {
IconButton(onClick = navigator.pop) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = "返回"
)
}
},
scrollBehavior = scrollBehavior,
)
BlurredBar(backdrop = blurBackdrop) {
TopAppBar(
title = "虚拟按钮排序",
color =
if (blurActive) Color.Transparent
else colorScheme.surface,
navigationIcon = {
IconButton(onClick = navigator.pop) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = "返回"
)
}
},
scrollBehavior = scrollBehavior,
)
}
},
) { pagePadding ->
VirtualButtonOrderPage(
contentPadding = pagePadding,
scrollBehavior = scrollBehavior,
)
Box(modifier = if (blurActive) Modifier.layerBackdrop(blurBackdrop) else Modifier) {
VirtualButtonOrderPage(
contentPadding = pagePadding,
scrollBehavior = scrollBehavior,
)
}
}
}
@@ -72,7 +87,6 @@ internal fun VirtualButtonOrderPage(
contentPadding: PaddingValues,
scrollBehavior: ScrollBehavior,
) {
val scope = rememberCoroutineScope()
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
val asBundleShared by appSettings.bundleState.collectAsState()
@@ -105,8 +119,8 @@ internal fun VirtualButtonOrderPage(
LazyColumn(
contentPadding = contentPadding,
scrollBehavior = scrollBehavior,
bottomInnerPadding = UiSpacing.PageBottom,
) {
// 按钮显示文本开关
item {
Card {
SwitchPreference(
@@ -130,8 +144,12 @@ internal fun VirtualButtonOrderPage(
ReorderableList.Item(
id = action.id,
icon = action.icon,
title = if (action.keycode == null) action.title else "${action.title} (${action.keycode})",
subtitle = if (item.showOutside) "显示在外部" else "显示在更多菜单内",
title =
if (action.keycode == null) action.title
else "${action.title} (${action.keycode})",
subtitle =
if (item.showOutside) "显示在外部"
else "显示在更多菜单内",
endActions = listOf(
ReorderableList.EndAction.Checkbox(
checked = item.showOutside,
@@ -139,16 +157,13 @@ internal fun VirtualButtonOrderPage(
onClick = {
val checked = !item.showOutside
buttonItems = buttonItems.map { current ->
if (current.action.id == action.id) {
if (current.action.id == action.id)
current.copy(showOutside = checked)
} else {
current
}
else current
}
asBundle = asBundle.copy(
virtualButtonsLayout = VirtualButtonActions.encodeStoredLayout(
buttonItems
)
virtualButtonsLayout = VirtualButtonActions
.encodeStoredLayout(buttonItems)
)
},
)
@@ -158,11 +173,11 @@ internal fun VirtualButtonOrderPage(
},
orientation = ReorderableList.Orientation.Column,
onSettle = { fromIndex, toIndex ->
buttonItems = buttonItems.toMutableList().apply {
add(toIndex, removeAt(fromIndex))
}
buttonItems = buttonItems.toMutableList()
.apply { add(toIndex, removeAt(fromIndex)) }
asBundle = asBundle.copy(
virtualButtonsLayout = VirtualButtonActions.encodeStoredLayout(buttonItems)
virtualButtonsLayout = VirtualButtonActions
.encodeStoredLayout(buttonItems)
)
},
)()

View File

@@ -0,0 +1,105 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.spring
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.luminance
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
@Composable
internal fun BgEffectBackground(
dynamicBackground: Boolean,
modifier: Modifier = Modifier,
bgModifier: Modifier = Modifier,
isFullSize: Boolean = false,
effectBackground: Boolean = true,
isOs3Effect: Boolean = true,
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(isOs3Effect) { BgEffectPainter(isOs3Effect) }
val animTime = rememberFrameTimeSeconds(dynamicBackground)
val isDark = colorScheme.background.luminance() < 0.5f
val deviceType = DeviceType.PHONE
val preset = remember(isDark, deviceType, isOs3Effect) {
BgEffectConfig.get(deviceType, isDark, isOs3Effect)
}
val colorStage = remember { Animatable(0f) }
LaunchedEffect(dynamicBackground, preset) {
if (!dynamicBackground) return@LaunchedEffect
var targetStage = 1f
while (isActive) {
delay((preset.colorInterpPeriod * 500).toLong())
colorStage.animateTo(
targetValue = targetStage,
animationSpec = spring(dampingRatio = 0.9f, stiffness = 35f),
)
targetStage += 1f
}
}
key(isOs3Effect) {
Canvas(
modifier = Modifier
.fillMaxSize()
.then(bgModifier),
) {
drawRect(surface)
if (effectBackground) {
val drawHeight = if (isFullSize) size.height else size.height * 0.78f
val stage = colorStage.value
val base = stage.toInt()
val fraction = stage - base
val getColors = { index: Int ->
when (index % 4) {
0 -> preset.colors2
1 -> preset.colors1
2 -> preset.colors2
3 -> preset.colors3
else -> preset.colors2
}
}
val start = getColors(base)
val end = getColors(base + 1)
val currentColors = FloatArray(16) { i ->
start[i] + (end[i] - start[i]) * fraction
}
painter.updateResolution(size.width, size.height)
painter.updatePresetIfNeeded(drawHeight, size.height, size.width, isDark)
painter.updateColors(currentColors)
painter.updateAnimTime(animTime())
drawRect(painter.brush, alpha = alpha())
}
}
}
content()
}
}

View File

@@ -0,0 +1,36 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
internal enum class DeviceType {
PHONE,
PAD,
}
internal object BgEffectConfig {
internal class Config(
val points: FloatArray,
val colors1: FloatArray,
val colors2: FloatArray,
val colors3: FloatArray,
val colorInterpPeriod: Float,
val lightOffset: Float,
val saturateOffset: Float,
val pointOffset: Float,
// OS3 specific
val shadowColorMulti: Float = 0.3f,
val shadowColorOffset: Float = 0.3f,
val shadowNoiseScale: Float = 5.0f,
)
internal fun get(
deviceType: DeviceType,
isDark: Boolean,
isOs3: Boolean,
): Config {
return if (isOs3) {
getOs3Config(deviceType, isDark)
} else {
getOs2Config(deviceType, isDark)
}
}
}

View File

@@ -0,0 +1,132 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
private val OS2_PHONE_LIGHT_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,
)
private val OS2_PHONE_LIGHT = BgEffectConfig.Config(
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
),
colors1 = OS2_PHONE_LIGHT_COLORS,
colors2 = OS2_PHONE_LIGHT_COLORS,
colors3 = OS2_PHONE_LIGHT_COLORS,
colorInterpPeriod = 100f,
lightOffset = 0.1f,
saturateOffset = 0.2f,
pointOffset = 0.1f,
)
private val OS2_PHONE_DARK_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,
)
private val OS2_PHONE_DARK = BgEffectConfig.Config(
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
),
colors1 = OS2_PHONE_DARK_COLORS,
colors2 = OS2_PHONE_DARK_COLORS,
colors3 = OS2_PHONE_DARK_COLORS,
colorInterpPeriod = 100f,
lightOffset = -0.1f,
saturateOffset = 0.2f,
pointOffset = 0.1f,
)
private val OS2_PAD_LIGHT_COLORS = floatArrayOf(
0.57f, 0.76f, 0.98f, 1.0f,
0.98f, 0.85f, 0.68f, 1.0f,
0.98f, 0.75f, 0.93f, 0.95f,
0.73f, 0.70f, 0.98f, 0.90f,
)
private val OS2_PAD_LIGHT = BgEffectConfig.Config(
points = floatArrayOf(
0.67f,
0.37f,
0.88f,
0.54f,
0.66f,
1.0f,
0.37f,
0.71f,
0.68f,
0.28f,
0.26f,
0.62f
),
colors1 = OS2_PAD_LIGHT_COLORS,
colors2 = OS2_PAD_LIGHT_COLORS,
colors3 = OS2_PAD_LIGHT_COLORS,
colorInterpPeriod = 100f,
lightOffset = 0.1f,
saturateOffset = 0f,
pointOffset = 0.1f,
)
private val OS2_PAD_DARK_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,
)
private val OS2_PAD_DARK = BgEffectConfig.Config(
points = floatArrayOf(
0.55f,
0.42f,
1.0f,
0.56f,
0.75f,
1.0f,
0.40f,
0.59f,
0.71f,
0.43f,
0.09f,
0.75f
),
colors1 = OS2_PAD_DARK_COLORS,
colors2 = OS2_PAD_DARK_COLORS,
colors3 = OS2_PAD_DARK_COLORS,
colorInterpPeriod = 100f,
lightOffset = -0.1f,
saturateOffset = 0.2f,
pointOffset = 0.1f,
)
internal fun getOs2Config(deviceType: DeviceType, isDark: Boolean): BgEffectConfig.Config {
return when (deviceType) {
DeviceType.PHONE -> if (isDark) OS2_PHONE_DARK else OS2_PHONE_LIGHT
DeviceType.PAD -> if (isDark) OS2_PAD_DARK else OS2_PAD_LIGHT
}
}

View File

@@ -0,0 +1,256 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
private val OS3_PHONE_LIGHT = BgEffectConfig.Config(
points = floatArrayOf(0.8f, 0.2f, 1.0f, 0.8f, 0.9f, 1.0f, 0.2f, 0.9f, 1.0f, 0.2f, 0.2f, 1.0f),
colors1 = floatArrayOf(
1.0f,
0.9f,
0.94f,
1.0f,
1.0f,
0.84f,
0.89f,
1.0f,
0.97f,
0.73f,
0.82f,
1.0f,
0.64f,
0.65f,
0.98f,
1.0f
),
colors2 = floatArrayOf(
0.58f,
0.74f,
1.0f,
1.0f,
1.0f,
0.9f,
0.93f,
1.0f,
0.74f,
0.76f,
1.0f,
1.0f,
0.97f,
0.77f,
0.84f,
1.0f
),
colors3 = floatArrayOf(
0.98f,
0.86f,
0.9f,
1.0f,
0.6f,
0.73f,
0.98f,
1.0f,
0.92f,
0.93f,
1.0f,
1.0f,
0.56f,
0.69f,
1.0f,
1.0f
),
colorInterpPeriod = 5.0f,
lightOffset = 0.1f,
saturateOffset = 0.2f,
pointOffset = 0.2f,
)
private val OS3_PHONE_DARK = BgEffectConfig.Config(
points = floatArrayOf(0.8f, 0.2f, 1.0f, 0.8f, 0.9f, 1.0f, 0.2f, 0.9f, 1.0f, 0.2f, 0.2f, 1.0f),
colors1 = floatArrayOf(
0.2f,
0.06f,
0.88f,
0.4f,
0.3f,
0.14f,
0.55f,
0.5f,
0.0f,
0.64f,
0.96f,
0.5f,
0.11f,
0.16f,
0.83f,
0.4f
),
colors2 = floatArrayOf(
0.07f,
0.15f,
0.79f,
0.5f,
0.62f,
0.21f,
0.67f,
0.5f,
0.06f,
0.25f,
0.84f,
0.5f,
0.0f,
0.2f,
0.78f,
0.5f
),
colors3 = floatArrayOf(
0.58f,
0.3f,
0.74f,
0.4f,
0.27f,
0.18f,
0.6f,
0.5f,
0.66f,
0.26f,
0.62f,
0.5f,
0.12f,
0.16f,
0.7f,
0.6f
),
colorInterpPeriod = 8.0f,
lightOffset = 0.0f,
saturateOffset = 0.17f,
pointOffset = 0.4f,
)
private val OS3_PAD_LIGHT = BgEffectConfig.Config(
points = floatArrayOf(0.8f, 0.2f, 1.0f, 0.8f, 0.9f, 1.0f, 0.2f, 0.9f, 1.0f, 0.2f, 0.2f, 1.0f),
colors1 = floatArrayOf(
0.99f,
0.77f,
0.86f,
1.0f,
0.74f,
0.76f,
1.0f,
1.0f,
0.72f,
0.74f,
1.0f,
1.0f,
0.98f,
0.76f,
0.8f,
1.0f
),
colors2 = floatArrayOf(
0.66f,
0.75f,
1.0f,
1.0f,
1.0f,
0.86f,
0.91f,
1.0f,
0.74f,
0.76f,
1.0f,
1.0f,
0.97f,
0.77f,
0.84f,
1.0f
),
colors3 = floatArrayOf(
0.97f,
0.79f,
0.85f,
1.0f,
0.65f,
0.68f,
0.98f,
1.0f,
0.66f,
0.77f,
1.0f,
1.0f,
0.72f,
0.73f,
0.98f,
1.0f
),
colorInterpPeriod = 7.0f,
lightOffset = 0.1f,
saturateOffset = 0.2f,
pointOffset = 0.2f,
)
private val OS3_PAD_DARK = BgEffectConfig.Config(
points = floatArrayOf(0.8f, 0.2f, 1.0f, 0.8f, 0.9f, 1.0f, 0.2f, 0.9f, 1.0f, 0.2f, 0.2f, 1.0f),
colors1 = floatArrayOf(
0.66f,
0.26f,
0.62f,
0.4f,
0.06f,
0.25f,
0.84f,
0.5f,
0.0f,
0.64f,
0.96f,
0.5f,
0.14f,
0.18f,
0.55f,
0.5f
),
colors2 = floatArrayOf(
0.07f,
0.15f,
0.79f,
0.5f,
0.11f,
0.16f,
0.83f,
0.5f,
0.06f,
0.25f,
0.84f,
0.5f,
0.66f,
0.26f,
0.62f,
0.5f
),
colors3 = floatArrayOf(
0.58f,
0.3f,
0.74f,
0.5f,
0.11f,
0.16f,
0.83f,
0.5f,
0.66f,
0.26f,
0.62f,
0.5f,
0.27f,
0.18f,
0.6f,
0.6f
),
colorInterpPeriod = 7.0f,
lightOffset = 0.0f,
saturateOffset = 0.0f,
pointOffset = 0.2f,
)
internal fun getOs3Config(deviceType: DeviceType, isDark: Boolean): BgEffectConfig.Config {
return when (deviceType) {
DeviceType.PHONE -> if (isDark) OS3_PHONE_DARK else OS3_PHONE_LIGHT
DeviceType.PAD -> if (isDark) OS3_PAD_DARK else OS3_PAD_LIGHT
}
}

View File

@@ -0,0 +1,127 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
import androidx.compose.ui.graphics.Brush
import top.yukonga.miuix.kmp.blur.RuntimeShader
import top.yukonga.miuix.kmp.blur.asBrush
internal class BgEffectPainter(
private val isOs3: Boolean = true,
) {
val runtimeShader by lazy {
val shaderCode = if (isOs3) OS3_BG_FRAG else OS2_BG_FRAG
RuntimeShader(shaderCode).also {
initStaticUniforms(it)
}
}
val brush: Brush by lazy { runtimeShader.asBrush() }
private val resolution = FloatArray(2)
private val bound = FloatArray(4)
private var animTime = Float.NaN
private var isDarkCached: Boolean? = null
private var deviceTypeCached: DeviceType? = null
private var presetApplied = false
private var deviceType = DeviceType.PHONE
private companion object {
private const val U_TRANSLATE_Y = 0f
private const val U_ALPHA_MULTI = 1f
private const val U_NOISE_SCALE = 1.5f
private const val U_POINT_RADIUS_MULTI = 1f
private const val U_ALPHA_OFFSET = 0.1f
private const val U_SHADOW_OFFSET = 0.01f
}
private fun initStaticUniforms(shader: RuntimeShader) {
shader.setFloatUniform("uTranslateY", U_TRANSLATE_Y)
shader.setFloatUniform("uNoiseScale", U_NOISE_SCALE)
shader.setFloatUniform("uPointRadiusMulti", U_POINT_RADIUS_MULTI)
shader.setFloatUniform("uAlphaMulti", U_ALPHA_MULTI)
if (isOs3) {
shader.setFloatUniform("uAlphaOffset", U_ALPHA_OFFSET)
shader.setFloatUniform("uShadowOffset", U_SHADOW_OFFSET)
}
}
fun setDeviceType(type: DeviceType) {
if (deviceType == type) return
deviceType = type
presetApplied = false
}
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 updateColors(colors: FloatArray) {
runtimeShader.setFloatUniform("uColors", colors)
}
fun updatePresetIfNeeded(
logoHeight: Float,
height: Float,
width: Float,
isDark: Boolean,
) {
if (presetApplied && isDarkCached == isDark && deviceTypeCached == deviceType) return
updateBound(logoHeight, height, width)
applyPreset(isDark)
isDarkCached = isDark
deviceTypeCached = deviceType
presetApplied = true
}
private fun applyPreset(isDark: Boolean) {
val preset = BgEffectConfig.get(deviceType, isDark, isOs3)
runtimeShader.setFloatUniform("uPoints", preset.points)
runtimeShader.setFloatUniform("uPointOffset", preset.pointOffset)
runtimeShader.setFloatUniform("uLightOffset", preset.lightOffset)
runtimeShader.setFloatUniform("uSaturateOffset", preset.saturateOffset)
runtimeShader.setFloatUniform("uBound", bound)
if (isOs3) {
runtimeShader.setFloatUniform("uShadowColorMulti", preset.shadowColorMulti)
runtimeShader.setFloatUniform("uShadowColorOffset", preset.shadowColorOffset)
runtimeShader.setFloatUniform("uShadowNoiseScale", preset.shadowNoiseScale)
}
}
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
}
}
}

View File

@@ -0,0 +1,31 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
@Composable
internal 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 }
}

View File

@@ -0,0 +1,104 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
internal 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

@@ -0,0 +1,140 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
internal const val OS3_BG_FRAG = """
uniform vec2 uResolution;
uniform shader uTex;
uniform shader uTexBitmap;
uniform vec2 uTexWH;
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;
uniform float uAlphaOffset;
uniform float uShadowColorMulti;
uniform float uShadowColorOffset;
uniform float uShadowNoiseScale;
uniform float uShadowOffset;
vec3 hsl2rgb(in vec3 c) {
vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0, 4.0, 2.0), 6.0)-3.0)-1.0, 0.0, 1.0);
return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0));
}
vec3 HueShift (in vec3 Color, in float Shift) {
vec3 P = vec3(0.55735)*dot(vec3(0.55735), Color);
vec3 U = Color-P;
vec3 V = cross(vec3(0.55735), U);
Color = U*cos(Shift*6.2832) + V*sin(Shift*6.2832) + P;
return vec3(Color);
}
vec3 rgb2hsl(in vec3 c){
float h = 0.0; float s = 0.0; float l = 0.0;
float r = c.r; float g = c.g; float b = c.b;
float cMin = min(r, min(g, b));
float cMax = max(r, max(g, b));
l = (cMax + cMin) / 2.0;
if (cMax > cMin) {
float cDelta = cMax - cMin;
s = l < .0 ? cDelta / (cMax + cMin) : cDelta / (2.0 - (cMax + cMin));
if (r == cMax) h = (g - b) / cDelta;
else if (g == cMax) h = 2.0 + (b - r) / cDelta;
else h = 4.0 + (r - g) / cDelta;
if (h < 0.0) h += 6.0;
h = h / 6.0;
}
return vec3(h, s, l);
}
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);
color.rgb /= color.a;
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;
if (uiColor.a < 0.01) {
fragColor = color;
} else {
fragColor = uiColor;
}
color += (1.0 / 255.0) * gradientNoise(fragCoord.xy) - (0.5 / 255.0);
return vec4(color.rgb*color.a, color.a);
}
"""

View File

@@ -2,16 +2,12 @@ package io.github.miuzarte.scrcpyforandroid.password
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
import android.os.Looper
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import io.github.miuzarte.scrcpyforandroid.nativecore.AdbMdnsDiscoverer
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
import io.github.miuzarte.scrcpyforandroid.storage.Storage
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.resume
@@ -30,7 +26,8 @@ object BiometricGate {
}
fun isDeviceSecure(): Boolean {
val manager = AppRuntime.context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
val manager = AppRuntime.context
.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
return manager.isDeviceSecure
}

View File

@@ -23,7 +23,7 @@ import top.yukonga.miuix.kmp.basic.SpinnerDefaults
import top.yukonga.miuix.kmp.basic.SpinnerEntry
import top.yukonga.miuix.kmp.basic.SpinnerItemImpl
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
@Composable
fun PasswordPickerPopupContent(
@@ -67,7 +67,7 @@ fun PasswordPickerPopupContent(
modifier = Modifier
.fillMaxWidth()
.padding(UiSpacing.PopupHorizontal),
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
color = colorScheme.onSurfaceVariantSummary,
fontWeight = FontWeight.Medium,
)
return@ListPopupColumn
@@ -79,7 +79,7 @@ fun PasswordPickerPopupContent(
modifier = Modifier
.fillMaxWidth()
.padding(UiSpacing.PopupHorizontal),
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
color = colorScheme.onSurfaceVariantSummary,
fontWeight = FontWeight.Medium,
)
return@ListPopupColumn

View File

@@ -6,7 +6,6 @@ import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
import io.github.miuzarte.scrcpyforandroid.storage.Storage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

View File

@@ -3,8 +3,9 @@ package io.github.miuzarte.scrcpyforandroid.scaffolds
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
@@ -14,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
@@ -29,11 +31,20 @@ fun LazyColumn(
itemSpacing: Dp = UiSpacing.PageItem,
horizontalPadding: Dp = UiSpacing.PageHorizontal,
verticalPadding: Dp = UiSpacing.PageVertical,
bottomInnerPadding: Dp? = null,
clearFocusOnTap: Boolean = true,
content: LazyListScope.() -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val focusManager = LocalFocusManager.current
val mergedContentPadding = PaddingValues(
start = contentPadding.calculateLeftPadding(layoutDirection) + horizontalPadding,
top = contentPadding.calculateTopPadding() + verticalPadding,
end = contentPadding.calculateRightPadding(layoutDirection) + horizontalPadding,
bottom = contentPadding.calculateBottomPadding() + verticalPadding,
)
LazyColumn(
modifier = modifier
.fillMaxSize()
@@ -48,11 +59,16 @@ fun LazyColumn(
if (scrollBehavior != null)
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
else Modifier
)
.padding(contentPadding),
),
state = state,
contentPadding = PaddingValues(horizontal = horizontalPadding, vertical = verticalPadding),
contentPadding = mergedContentPadding,
verticalArrangement = Arrangement.spacedBy(itemSpacing),
content = content,
)
) {
content()
bottomInnerPadding?.let { padding ->
(padding - itemSpacing)
.takeIf { it > 0.dp }
?.let { item { Spacer(Modifier.height(it)) } }
}
}
}

View File

@@ -22,7 +22,8 @@ import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.overlay.OverlayDialog
import top.yukonga.miuix.kmp.preference.ArrowPreference
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
@Composable
fun SuperSlider(
@@ -69,8 +70,8 @@ fun SuperSlider(
val text = if (shouldShowUnit) "$valueText $unit" else valueText
Text(
text = text,
fontSize = MiuixTheme.textStyles.body2.fontSize,
color = MiuixTheme.colorScheme.onSurfaceVariantActions,
fontSize = textStyles.body2.fontSize,
color = colorScheme.onSurfaceVariantActions,
)
},
enabled = enabled,

View File

@@ -22,7 +22,8 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
/**
* A wrapped [TextField] component with state change callbacks.
@@ -44,7 +45,6 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme
* @param onFocusLost The callback to be called when the text field loses focus.
* @param insideMargin The margin inside the [TextField].
* @param backgroundColor The background color of the [TextField].
* @param cornerRadius The corner radius of the [TextField].
* @param labelColor The color of the label.
* @param borderColor The color of the border when the [TextField] is focused.
* @param textStyle The text style to be applied to the [TextField].
@@ -70,12 +70,11 @@ fun SuperTextField(
onFocusGained: (() -> Unit)? = null,
onFocusLost: (() -> Unit)? = null,
insideMargin: DpSize = DpSize(16.dp, 16.dp),
backgroundColor: Color = MiuixTheme.colorScheme.secondaryContainer,
cornerRadius: Dp = 16.dp,
labelColor: Color = MiuixTheme.colorScheme.onSecondaryContainer,
borderColor: Color = MiuixTheme.colorScheme.primary,
textStyle: TextStyle = MiuixTheme.textStyles.main,
cursorBrush: Brush = SolidColor(MiuixTheme.colorScheme.primary),
backgroundColor: Color = colorScheme.secondaryContainer,
labelColor: Color = colorScheme.onSecondaryContainer,
borderColor: Color = colorScheme.primary,
textStyle: TextStyle = textStyles.main,
cursorBrush: Brush = SolidColor(colorScheme.primary),
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
) {
@@ -109,7 +108,6 @@ fun SuperTextField(
visualTransformation = visualTransformation,
insideMargin = insideMargin,
backgroundColor = backgroundColor,
cornerRadius = cornerRadius,
labelColor = labelColor,
borderColor = borderColor,
textStyle = textStyle,

View File

@@ -1,8 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.services
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
import io.github.miuzarte.scrcpyforandroid.nativecore.AdbMdnsDiscoverer
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
@@ -11,7 +9,6 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
object AppRuntime {
private lateinit var appContext: Context
@RequiresApi(Build.VERSION_CODES.R)
fun init(context: Context) {
appContext = context.applicationContext
AdbMdnsDiscoverer.init(appContext)

View File

@@ -5,15 +5,10 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import androidx.annotation.RequiresApi
import io.github.miuzarte.scrcpyforandroid.password.BiometricGate
import io.github.miuzarte.scrcpyforandroid.storage.Storage
import kotlinx.coroutines.runBlocking
// not working in MIUI
class PictureInPictureActionReceiver : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action != ACTION_STOP_SCRCPY) return
val pendingResult = goAsync()

View File

@@ -22,6 +22,22 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
booleanPreferencesKey("monet"),
false
)
val BLUR = Pair(
booleanPreferencesKey("blur"),
true,
)
val FLOATING_BOTTOM_BAR = Pair(
booleanPreferencesKey("floating_bottom_bar"),
false,
)
val FLOATING_BOTTOM_BAR_BLUR = Pair(
booleanPreferencesKey("floating_bottom_bar_blur"),
false,
)
val SMOOTH_CORNER = Pair(
booleanPreferencesKey("smooth_corner"),
false,
)
val FULLSCREEN_DEBUG_INFO = Pair(
booleanPreferencesKey("fullscreen_debug_info"),
false
@@ -100,6 +116,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
// Theme Settings
val themeBaseIndex by setting(THEME_BASE_INDEX)
val monet by setting(MONET)
val blur by setting(BLUR)
val floatingBottomBar by setting(FLOATING_BOTTOM_BAR)
val floatingBottomBarBlur by setting(FLOATING_BOTTOM_BAR_BLUR)
val smoothCorner by setting(SMOOTH_CORNER)
// Scrcpy Settings
val fullscreenDebugInfo by setting(FULLSCREEN_DEBUG_INFO)
@@ -129,6 +149,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
data class Bundle(
val themeBaseIndex: Int,
val monet: Boolean,
val blur: Boolean,
val floatingBottomBar: Boolean,
val floatingBottomBarBlur: Boolean,
val smoothCorner: Boolean,
val fullscreenDebugInfo: Boolean,
val showFullscreenVirtualButtons: Boolean,
val showFullscreenFloatingButton: Boolean,
@@ -153,6 +177,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
private val bundleFields = arrayOf(
bundleField(THEME_BASE_INDEX) { bundle: Bundle -> bundle.themeBaseIndex },
bundleField(MONET) { bundle: Bundle -> bundle.monet },
bundleField(BLUR) { bundle: Bundle -> bundle.blur },
bundleField(FLOATING_BOTTOM_BAR) { bundle: Bundle -> bundle.floatingBottomBar },
bundleField(FLOATING_BOTTOM_BAR_BLUR) { bundle: Bundle -> bundle.floatingBottomBarBlur },
bundleField(SMOOTH_CORNER) { bundle: Bundle -> bundle.smoothCorner },
bundleField(FULLSCREEN_DEBUG_INFO) { bundle: Bundle -> bundle.fullscreenDebugInfo },
bundleField(SHOW_FULLSCREEN_VIRTUAL_BUTTONS) { bundle: Bundle -> bundle.showFullscreenVirtualButtons },
bundleField(SHOW_FULLSCREEN_FLOATING_BUTTON) { bundle: Bundle -> bundle.showFullscreenFloatingButton },
@@ -178,6 +206,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
private fun bundleFromPreferences(preferences: Preferences) = Bundle(
themeBaseIndex = preferences.read(THEME_BASE_INDEX),
monet = preferences.read(MONET),
blur = preferences.read(BLUR),
floatingBottomBar = preferences.read(FLOATING_BOTTOM_BAR),
floatingBottomBarBlur = preferences.read(FLOATING_BOTTOM_BAR_BLUR),
smoothCorner = preferences.read(SMOOTH_CORNER),
fullscreenDebugInfo = preferences.read(FULLSCREEN_DEBUG_INFO),
showFullscreenVirtualButtons = preferences.read(SHOW_FULLSCREEN_VIRTUAL_BUTTONS),
showFullscreenFloatingButton = preferences.read(SHOW_FULLSCREEN_FLOATING_BUTTON),

View File

@@ -0,0 +1,52 @@
package io.github.miuzarte.scrcpyforandroid.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalDensity
import top.yukonga.miuix.kmp.blur.BlendColorEntry
import top.yukonga.miuix.kmp.blur.BlurColors
import top.yukonga.miuix.kmp.blur.LayerBackdrop
import top.yukonga.miuix.kmp.blur.isRenderEffectSupported
import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop
import top.yukonga.miuix.kmp.blur.textureBlur
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
val LocalEnableBlur = staticCompositionLocalOf { false }
val LocalEnableFloatingBottomBar = staticCompositionLocalOf { false }
val LocalEnableFloatingBottomBarBlur = staticCompositionLocalOf { false }
@Composable
fun rememberBlurBackdrop(enableBlur: Boolean): LayerBackdrop? {
if (!enableBlur || !isRenderEffectSupported()) return null
val surfaceColor = colorScheme.surface
return rememberLayerBackdrop {
drawRect(surfaceColor)
drawContent()
}
}
@Composable
fun BlurredBar(
backdrop: LayerBackdrop?,
content: @Composable () -> Unit,
) {
Box(
modifier =
if (backdrop != null) Modifier.textureBlur(
backdrop = backdrop,
shape = RectangleShape,
blurRadius = 25f * LocalDensity.current.density,
colors = BlurColors(
blendColors = listOf(
BlendColorEntry(color = colorScheme.surface.copy(alpha = 0.87f)),
),
),
)
else Modifier,
) {
content()
}
}

View File

@@ -0,0 +1,379 @@
package io.github.miuzarte.scrcpyforandroid.ui.component
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastRoundToInt
import androidx.compose.ui.util.lerp
import com.kyant.backdrop.Backdrop
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import com.kyant.backdrop.drawBackdrop
import com.kyant.backdrop.effects.blur
import com.kyant.backdrop.effects.lens
import com.kyant.backdrop.effects.vibrancy
import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.shadow.InnerShadow
import com.kyant.backdrop.shadow.Shadow
import io.github.miuzarte.scrcpyforandroid.ui.component.miuix.animation.DampedDragAnimation
import io.github.miuzarte.scrcpyforandroid.ui.component.miuix.animation.InteractiveHighlight
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import kotlin.math.abs
import kotlin.math.sign
val LocalFloatingBottomBarTabScale = staticCompositionLocalOf { { 1f } }
@Composable
fun RowScope.FloatingBottomBarItem(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
val scale = LocalFloatingBottomBarTabScale.current
Column(
modifier
.clip(CircleShape)
.clickable(
interactionSource = null,
indication = null,
role = Role.Tab,
onClick = onClick
)
.fillMaxHeight()
.weight(1f)
.graphicsLayer {
val currentScale = scale()
scaleX = currentScale
scaleY = currentScale
},
verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
content = content
)
}
@Composable
fun FloatingBottomBar(
modifier: Modifier = Modifier,
selectedIndex: () -> Int,
onSelected: (index: Int) -> Unit,
backdrop: Backdrop,
tabsCount: Int,
isBlurEnabled: Boolean = true,
content: @Composable RowScope.() -> Unit,
) {
val isInLightTheme = colorScheme.background.luminance() >= 0.5f
val accentColor = colorScheme.primary
val containerColor = if (isBlurEnabled) {
colorScheme.surfaceContainer.copy(0.4f)
} else {
colorScheme.surfaceContainer
}
val tabsBackdrop = rememberLayerBackdrop()
val density = LocalDensity.current
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val animationScope = rememberCoroutineScope()
var tabWidthPx by remember { mutableFloatStateOf(0f) }
var totalWidthPx by remember { mutableFloatStateOf(0f) }
val offsetAnimation = remember { Animatable(0f) }
val panelOffset by remember(density) {
derivedStateOf {
if (totalWidthPx == 0f) {
0f
} else {
val fraction = (offsetAnimation.value / totalWidthPx).fastCoerceIn(-1f, 1f)
with(density) {
4f.dp.toPx() * fraction.sign * EaseOut.transform(abs(fraction))
}
}
}
}
var currentIndex by remember(selectedIndex) { mutableIntStateOf(selectedIndex()) }
class DampedDragAnimationHolder {
var instance: DampedDragAnimation? = null
}
val holder = remember { DampedDragAnimationHolder() }
val dampedDragAnimation = remember(animationScope, tabsCount, density, isLtr) {
DampedDragAnimation(
animationScope = animationScope,
initialValue = selectedIndex().toFloat(),
valueRange = 0f..(tabsCount - 1).toFloat(),
visibilityThreshold = 0.001f,
initialScale = 1f,
pressedScale = 78f / 56f,
canDrag = { offset ->
val anim = holder.instance ?: return@DampedDragAnimation true
if (tabWidthPx == 0f) return@DampedDragAnimation false
val currentValue = anim.value
val indicatorX = currentValue * tabWidthPx
val padding = with(density) { 4.dp.toPx() }
val globalTouchX = if (isLtr) {
val touchX = indicatorX + offset.x
padding + touchX
} else {
val touchX = totalWidthPx - padding - tabWidthPx - indicatorX + offset.x
touchX
}
globalTouchX in 0f..totalWidthPx
},
onDragStarted = {},
onDragStopped = {
val targetIndex = targetValue.fastRoundToInt().fastCoerceIn(0, tabsCount - 1)
currentIndex = targetIndex
animateToValue(targetIndex.toFloat())
animationScope.launch {
offsetAnimation.animateTo(0f, spring(1f, 300f, 0.5f))
}
},
onDrag = { _, dragAmount ->
if (tabWidthPx > 0) {
updateValue(
(targetValue + dragAmount.x / tabWidthPx * if (isLtr) 1f else -1f)
.fastCoerceIn(0f, (tabsCount - 1).toFloat())
)
animationScope.launch {
offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x)
}
}
}
).also { holder.instance = it }
}
LaunchedEffect(selectedIndex) {
snapshotFlow { selectedIndex() }.collectLatest { currentIndex = it }
}
LaunchedEffect(dampedDragAnimation) {
snapshotFlow { currentIndex }.drop(1).collectLatest { index ->
dampedDragAnimation.animateToValue(index.toFloat())
onSelected(index)
}
}
val interactiveHighlight = remember(animationScope, tabWidthPx) {
InteractiveHighlight(
animationScope = animationScope,
position = { size, _ ->
Offset(
if (isLtr) {
(dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset
} else {
size.width - (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset
},
size.height / 2f
)
}
)
}
Box(
modifier = modifier.width(IntrinsicSize.Min),
contentAlignment = Alignment.CenterStart
) {
Row(
Modifier
.onGloballyPositioned { coords ->
totalWidthPx = coords.size.width.toFloat()
val contentWidthPx = totalWidthPx - with(density) { 8.dp.toPx() }
tabWidthPx = contentWidthPx / tabsCount
}
.graphicsLayer { translationX = panelOffset }
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {}
)
.drawBackdrop(
backdrop = backdrop,
shape = { CircleShape },
effects = {
if (isBlurEnabled) {
vibrancy()
blur(8f.dp.toPx())
lens(24f.dp.toPx(), 24f.dp.toPx())
}
},
highlight = {
Highlight.Default.copy(alpha = if (isBlurEnabled) 1f else 0f)
},
shadow = {
Shadow.Default.copy(
color = Color.Black.copy(if (isInLightTheme) 0.1f else 0.2f),
)
},
layerBlock = {
if (isBlurEnabled) {
val progress = dampedDragAnimation.pressProgress
val scale = lerp(1f, 1f + 16f.dp.toPx() / size.width, progress)
scaleX = scale
scaleY = scale
}
},
onDrawSurface = { drawRect(containerColor) }
)
.then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier)
.height(64.dp)
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically,
content = content
)
CompositionLocalProvider(
LocalFloatingBottomBarTabScale provides {
if (isBlurEnabled) lerp(1f, 1.2f, dampedDragAnimation.pressProgress)
else 1f
}
) {
Row(
Modifier
.clearAndSetSemantics {}
.alpha(0f)
.layerBackdrop(tabsBackdrop)
.graphicsLayer { translationX = panelOffset }
.drawBackdrop(
backdrop = backdrop,
shape = { CircleShape },
effects = {
if (isBlurEnabled) {
val progress = dampedDragAnimation.pressProgress
vibrancy()
blur(8f.dp.toPx())
lens(24f.dp.toPx() * progress, 24f.dp.toPx() * progress)
}
},
highlight = {
Highlight.Default.copy(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f)
},
onDrawSurface = { drawRect(containerColor) }
)
.then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier)
.height(56.dp)
.padding(horizontal = 4.dp)
.graphicsLayer(colorFilter = ColorFilter.tint(accentColor)),
verticalAlignment = Alignment.CenterVertically,
content = content
)
}
if (tabWidthPx > 0f) {
Box(
Modifier
.padding(horizontal = 4.dp)
.graphicsLayer {
val contentWidth = totalWidthPx - with(density) { 8.dp.toPx() }
val singleTabWidth = contentWidth / tabsCount
val progressOffset = dampedDragAnimation.value * singleTabWidth
translationX = if (isLtr) {
progressOffset + panelOffset
} else {
-progressOffset + panelOffset
}
}
.then(if (isBlurEnabled) interactiveHighlight.gestureModifier else Modifier)
.then(dampedDragAnimation.modifier)
.drawBackdrop(
backdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop),
shape = { CircleShape },
effects = {
if (isBlurEnabled) {
val progress = dampedDragAnimation.pressProgress
lens(10f.dp.toPx() * progress, 14f.dp.toPx() * progress, true)
}
},
highlight = {
Highlight.Default.copy(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f)
},
shadow = { Shadow(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f) },
innerShadow = {
InnerShadow(
radius = 8f.dp * dampedDragAnimation.pressProgress,
alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f
)
},
layerBlock = {
if (isBlurEnabled) {
scaleX = dampedDragAnimation.scaleX
scaleY = dampedDragAnimation.scaleY
val velocity = dampedDragAnimation.velocity / 10f
scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f)
scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f)
}
},
onDrawSurface = {
val progress =
if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f
drawRect(
color = if (isInLightTheme) {
Color.Black.copy(0.1f)
} else {
Color.White.copy(0.1f)
},
alpha = 1f - progress
)
drawRect(
Color.Black.copy(alpha = 0.03f * progress)
)
}
)
.height(56.dp)
.width(with(density) { ((totalWidthPx - 8.dp.toPx()) / tabsCount).toDp() })
)
}
}
}

View File

@@ -0,0 +1,141 @@
package io.github.miuzarte.scrcpyforandroid.ui.component.miuix.animation
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.spring
import androidx.compose.foundation.MutatorMutex
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.unit.IntSize
import io.github.miuzarte.scrcpyforandroid.ui.component.miuix.modifier.inspectDragGestures
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.android.awaitFrame
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlin.math.abs
class DampedDragAnimation(
private val animationScope: CoroutineScope,
val initialValue: Float,
val valueRange: ClosedRange<Float>,
val visibilityThreshold: Float,
val initialScale: Float,
val pressedScale: Float,
val canDrag: (Offset) -> Boolean = { true },
val onDragStarted: DampedDragAnimation.(position: Offset) -> Unit,
val onDragStopped: DampedDragAnimation.() -> Unit,
val onDrag: DampedDragAnimation.(size: IntSize, dragAmount: Offset) -> Unit,
) {
private val valueAnimationSpec = spring(1f, 1000f, visibilityThreshold)
private val velocityAnimationSpec = spring(0.5f, 300f, visibilityThreshold * 10f)
private val pressProgressAnimationSpec = spring(1f, 1000f, 0.001f)
private val scaleXAnimationSpec = spring(0.6f, 250f, 0.001f)
private val scaleYAnimationSpec = spring(0.7f, 250f, 0.001f)
private val valueAnimation = Animatable(initialValue, visibilityThreshold)
private val velocityAnimation = Animatable(0f, 5f)
private val pressProgressAnimation = Animatable(0f, 0.001f)
private val scaleXAnimation = Animatable(initialScale, 0.001f)
private val scaleYAnimation = Animatable(initialScale, 0.001f)
private val mutatorMutex = MutatorMutex()
private val velocityTracker = VelocityTracker()
val value: Float get() = valueAnimation.value
val targetValue: Float get() = valueAnimation.targetValue
val pressProgress: Float get() = pressProgressAnimation.value
val scaleX: Float get() = scaleXAnimation.value
val scaleY: Float get() = scaleYAnimation.value
val velocity: Float get() = velocityAnimation.value
val modifier: Modifier = Modifier.pointerInput(Unit) {
inspectDragGestures(
onDragStart = { down ->
onDragStarted(down.position)
press()
},
onDragEnd = {
onDragStopped()
release()
},
onDragCancel = {
onDragStopped()
release()
}
) { change, dragAmount ->
val position = change.position
val previousPosition = change.previousPosition
val isInside = canDrag(position)
val wasInside = canDrag(previousPosition)
if (isInside && wasInside) {
onDrag(size, dragAmount)
}
}
}
fun press() {
velocityTracker.resetTracking()
animationScope.launch {
launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) }
launch { scaleXAnimation.animateTo(pressedScale, scaleXAnimationSpec) }
launch { scaleYAnimation.animateTo(pressedScale, scaleYAnimationSpec) }
}
}
fun release() {
animationScope.launch {
awaitFrame()
if (value != targetValue) {
val threshold = (valueRange.endInclusive - valueRange.start) * 0.025f
snapshotFlow { valueAnimation.value }
.filter { abs(it - valueAnimation.targetValue) < threshold }
.first()
}
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
launch { scaleXAnimation.animateTo(initialScale, scaleXAnimationSpec) }
launch { scaleYAnimation.animateTo(initialScale, scaleYAnimationSpec) }
}
}
fun updateValue(value: Float) {
val targetValue = value.coerceIn(valueRange)
animationScope.launch {
launch {
valueAnimation.animateTo(
targetValue,
valueAnimationSpec
) { updateVelocity() }
}
}
}
fun animateToValue(value: Float) {
animationScope.launch {
mutatorMutex.mutate {
press()
val targetValue = value.coerceIn(valueRange)
launch { valueAnimation.animateTo(targetValue, valueAnimationSpec) }
if (velocity != 0f) {
launch { velocityAnimation.animateTo(0f, velocityAnimationSpec) }
}
release()
}
}
}
private fun updateVelocity() {
velocityTracker.addPosition(
System.currentTimeMillis(),
Offset(value, 0f)
)
val targetVelocity =
velocityTracker.calculateVelocity().x / (valueRange.endInclusive - valueRange.start)
animationScope.launch { velocityAnimation.animateTo(targetVelocity, velocityAnimationSpec) }
}
}

View File

@@ -0,0 +1,105 @@
package io.github.miuzarte.scrcpyforandroid.ui.component.miuix.animation
import android.annotation.SuppressLint
import android.graphics.RuntimeShader
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.spring
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.util.fastCoerceIn
import io.github.miuzarte.scrcpyforandroid.ui.component.miuix.modifier.inspectDragGestures
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.intellij.lang.annotations.Language
@SuppressLint("NewApi")
class InteractiveHighlight(
val animationScope: CoroutineScope,
val position: (size: Size, offset: Offset) -> Offset = { _, offset -> offset },
) {
private val pressProgressAnimationSpec = spring(0.5f, 300f, 0.001f)
private val positionAnimationSpec = spring(0.5f, 300f, Offset.VisibilityThreshold)
private val pressProgressAnimation = Animatable(0f, 0.001f)
private val positionAnimation =
Animatable(Offset.Zero, Offset.VectorConverter, Offset.VisibilityThreshold)
private var startPosition = Offset.Zero
val offset: Offset get() = positionAnimation.value - startPosition
@Language("AGSL")
private val shader = RuntimeShader(
"""
uniform float2 size;
layout(color) uniform half4 color;
uniform float radius;
uniform float2 position;
half4 main(float2 coord) {
float dist = distance(coord, position);
float intensity = smoothstep(radius, radius * 0.5, dist);
return color * intensity;
}"""
)
val modifier: Modifier = Modifier.drawWithContent {
val progress = pressProgressAnimation.value
if (progress > 0f) {
drawRect(
Color.White.copy(0.06f * progress),
blendMode = BlendMode.Plus
)
shader.apply {
val shaderPosition = position(size, positionAnimation.value)
val posX = shaderPosition.x.fastCoerceIn(0f, size.width)
val poxY = shaderPosition.y.fastCoerceIn(0f, size.height)
setFloatUniform("size", size.width, size.height)
setColorUniform("color", Color.White.copy(0.12f * progress).toArgb())
setFloatUniform("radius", size.minDimension * 1.2f)
setFloatUniform("position", posX, poxY)
}
drawRect(
ShaderBrush(shader),
blendMode = BlendMode.Plus
)
}
drawContent()
}
val gestureModifier: Modifier = Modifier.pointerInput(animationScope) {
inspectDragGestures(
onDragStart = { down ->
startPosition = down.position
animationScope.launch {
launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) }
launch { positionAnimation.snapTo(startPosition) }
}
},
onDragEnd = {
animationScope.launch {
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) }
}
},
onDragCancel = {
animationScope.launch {
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) }
}
}
) { change, _ ->
animationScope.launch { positionAnimation.snapTo(change.position) }
}
}
}

View File

@@ -0,0 +1,78 @@
package io.github.miuzarte.scrcpyforandroid.ui.component.miuix.modifier
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.util.fastFirstOrNull
suspend fun PointerInputScope.inspectDragGestures(
onDragStart: (down: PointerInputChange) -> Unit = {},
onDragEnd: (change: PointerInputChange) -> Unit = {},
onDragCancel: () -> Unit = {},
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,
) {
awaitEachGesture {
val initialDown = awaitFirstDown(false, PointerEventPass.Initial)
val down = awaitFirstDown(false)
onDragStart(down)
onDrag(initialDown, Offset.Zero)
val upEvent = drag(
pointerId = initialDown.id,
onDrag = { onDrag(it, it.positionChange()) }
)
if (upEvent == null) {
onDragCancel()
} else {
onDragEnd(upEvent)
}
}
}
private suspend inline fun AwaitPointerEventScope.drag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit,
): PointerInputChange? {
val isPointerUp = currentEvent.changes.fastFirstOrNull { it.id == pointerId }?.pressed != true
if (isPointerUp) {
return null
}
var pointer = pointerId
while (true) {
val change = awaitDragOrUp(pointer) ?: return null
if (change.isConsumed) return null
if (change.changedToUpIgnoreConsumed()) return change
onDrag(change)
pointer = change.id
}
}
private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
pointerId: PointerId,
): PointerInputChange? {
var pointer = pointerId
while (true) {
val event = awaitPointerEvent()
val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
if (dragEvent.changedToUpIgnoreConsumed()) {
val otherDown = event.changes.fastFirstOrNull { it.pressed }
if (otherDown == null) {
return dragEvent
}
pointer = otherDown.id
} else {
val hasDragged = dragEvent.previousPosition != dragEvent.position
if (hasDragged) {
return dragEvent
}
}
}
}

View File

@@ -69,6 +69,7 @@ import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
import io.github.miuzarte.scrcpyforandroid.constants.Defaults
import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.haptics.LocalAppHaptics
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider
@@ -101,7 +102,7 @@ import top.yukonga.miuix.kmp.preference.CheckboxLocation
import top.yukonga.miuix.kmp.preference.CheckboxPreference
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
import top.yukonga.miuix.kmp.preference.SwitchPreference
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.isDynamicColor
import top.yukonga.miuix.kmp.utils.PressFeedbackType
import kotlin.math.roundToInt
@@ -132,17 +133,17 @@ internal fun StatusCard(
val spec = when {
streaming && sessionInfo != null -> {
val streamCardColor = when {
isDynamicColor -> MiuixTheme.colorScheme.secondaryContainer
isDynamicColor -> colorScheme.secondaryContainer
isDarkTheme -> Color(0xFF1A3825)
else -> Color(0xFFDFFAE4)
}
val streamTextColor = when {
isDynamicColor -> MiuixTheme.colorScheme.onSecondaryContainer
isDynamicColor -> colorScheme.onSecondaryContainer
isDarkTheme -> Color.White
else -> Color(0xFF111111)
}
val streamIconColor = if (isDynamicColor) {
MiuixTheme.colorScheme.primary.copy(alpha = 0.8f)
colorScheme.primary.copy(alpha = 0.8f)
} else {
Color(0xFF36D167)
}
@@ -171,11 +172,11 @@ internal fun StatusCard(
big = StatusBigCardSpec(
title = "ADB 已连接",
subtitle = cleanStatusLine,
containerColor = MiuixTheme.colorScheme.primaryContainer,
titleColor = MiuixTheme.colorScheme.onPrimaryContainer,
subtitleColor = MiuixTheme.colorScheme.onPrimaryContainer,
containerColor = colorScheme.primaryContainer,
titleColor = colorScheme.onPrimaryContainer,
subtitleColor = colorScheme.onPrimaryContainer,
icon = Icons.Rounded.Wifi,
iconTint = MiuixTheme.colorScheme.primary.copy(alpha = 0.6f),
iconTint = colorScheme.primary.copy(alpha = 0.6f),
),
firstSmall = StatusSmallCardSpec(
"当前设备",
@@ -191,11 +192,11 @@ internal fun StatusCard(
big = StatusBigCardSpec(
title = "ADB 未连接",
subtitle = "",
containerColor = MiuixTheme.colorScheme.secondaryContainer,
titleColor = MiuixTheme.colorScheme.onSecondaryContainer,
subtitleColor = MiuixTheme.colorScheme.onSecondaryContainer,
containerColor = colorScheme.secondaryContainer,
titleColor = colorScheme.onSecondaryContainer,
subtitleColor = colorScheme.onSecondaryContainer,
icon = Icons.Rounded.LinkOff,
iconTint = MiuixTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.6f),
iconTint = colorScheme.onSecondaryContainer.copy(alpha = 0.6f),
),
firstSmall = StatusSmallCardSpec(
"当前设备",
@@ -983,8 +984,8 @@ internal fun DeviceTile(
Card(
colors = CardDefaults.defaultColors(
color =
if (isConnected) MiuixTheme.colorScheme.surfaceContainer
else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f),
if (isConnected) colorScheme.surfaceContainer
else colorScheme.surfaceContainer.copy(alpha = 0.6f),
),
pressFeedbackType = if (!editing) PressFeedbackType.Sink else PressFeedbackType.None,
onClick = haptics.contextClick,
@@ -1015,7 +1016,7 @@ internal fun DeviceTile(
.background(
color =
if (isConnected) Color(0xFF44C74F)
else MiuixTheme.colorScheme.outline,
else colorScheme.outline,
shape = CircleShape,
),
)
@@ -1027,12 +1028,12 @@ internal fun DeviceTile(
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MiuixTheme.colorScheme.onSurface,
color = colorScheme.onSurface,
)
Text(
"${device.host}:${device.port}",
fontSize = 13.sp,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
color = colorScheme.onSurfaceVariantSummary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@@ -1206,14 +1207,16 @@ internal fun QuickConnectCard(
onAddDevice: () -> Unit,
enabled: Boolean = true,
) {
val haptics = LocalAppHaptics.current
val focusManager = LocalFocusManager.current
Card(
colors = CardDefaults.defaultColors(color = MiuixTheme.colorScheme.primaryContainer),
colors = CardDefaults.defaultColors(color = colorScheme.primaryContainer),
pressFeedbackType =
if (enabled) PressFeedbackType.Tilt
else PressFeedbackType.None,
insideMargin = PaddingValues(UiSpacing.Content),
onClick = haptics.contextClick,
) {
Column(verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical)) {
Row(
@@ -1223,12 +1226,12 @@ internal fun QuickConnectCard(
Icon(
Icons.Rounded.AddLink,
contentDescription = "快速连接",
tint = MiuixTheme.colorScheme.onPrimaryContainer,
tint = colorScheme.onPrimaryContainer,
)
Text(
"快速连接",
fontWeight = FontWeight.Bold,
color = MiuixTheme.colorScheme.onPrimaryContainer,
color = colorScheme.onPrimaryContainer,
)
}
SuperTextField(

View File

@@ -30,7 +30,7 @@ import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Checkbox
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
class ReorderableList(
private val itemsProvider: () -> List<Item>,
@@ -114,12 +114,12 @@ class ReorderableList(
Column {
Text(
text = item.title,
color = MiuixTheme.colorScheme.onSurface,
color = colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
)
if (item.subtitle.isNotBlank()) Text(
text = item.subtitle,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
color = colorScheme.onSurfaceVariantSummary,
fontSize = 13.sp,
)
}
@@ -152,7 +152,7 @@ class ReorderableList(
Icon(
Icons.Rounded.DragIndicator,
contentDescription = "拖动排序",
tint = MiuixTheme.colorScheme.onSurfaceVariantSummary,
tint = colorScheme.onSurfaceVariantSummary,
)
}
}
@@ -218,7 +218,7 @@ class ReorderableList(
Icon(
Icons.Rounded.DragIndicator,
contentDescription = "拖动排序",
tint = MiuixTheme.colorScheme.onSurfaceVariantSummary,
tint = colorScheme.onSurfaceVariantSummary,
)
}
}
@@ -226,13 +226,13 @@ class ReorderableList(
Spacer(Modifier.padding(UiSpacing.ContentVertical))
Text(
text = item.title,
color = MiuixTheme.colorScheme.onSurface,
color = colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
)
if (item.subtitle.isNotBlank()) {
Text(
text = item.subtitle,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
color = colorScheme.onSurfaceVariantSummary,
fontSize = 13.sp,
)
}
@@ -266,7 +266,7 @@ private fun EndActionView(
contentDescription = action.contentDescription.ifBlank {
fallbackContentDescription
},
tint = MiuixTheme.colorScheme.onSurfaceVariantSummary,
tint = colorScheme.onSurfaceVariantSummary,
)
}
}

View File

@@ -30,7 +30,7 @@ import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults.defaultColors
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.utils.PressFeedbackType
@Immutable
@@ -122,7 +122,7 @@ internal fun StatusCardLayout(
Text(
text = busyLabel,
fontSize = 12.sp,
color = MiuixTheme.colorScheme.primary,
color = colorScheme.primary,
)
}
}
@@ -164,13 +164,13 @@ private fun StatusMetricCard(spec: StatusSmallCardSpec, modifier: Modifier) {
text = spec.title,
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
color = colorScheme.onSurfaceVariantSummary,
)
Text(
text = spec.value,
fontSize = 24.sp,
fontWeight = FontWeight.SemiBold,
color = MiuixTheme.colorScheme.onSurface,
color = colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)

View File

@@ -66,7 +66,7 @@ import top.yukonga.miuix.kmp.basic.SpinnerEntry
import top.yukonga.miuix.kmp.basic.SpinnerItemImpl
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.overlay.OverlayListPopup
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
enum class VirtualButtonAction(
val id: String,
@@ -216,10 +216,10 @@ class VirtualButtonBar(
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
) {
val haptics = LocalAppHaptics.current
val activeContainerColor = MiuixTheme.colorScheme.primary
val disabledContainerColor = MiuixTheme.colorScheme.primary.copy(alpha = 0.35f)
val activeContentColor = MiuixTheme.colorScheme.onPrimary
val disabledContentColor = MiuixTheme.colorScheme.onPrimary.copy(alpha = 0.45f)
val activeContainerColor = colorScheme.primary
val disabledContainerColor = colorScheme.primary.copy(alpha = 0.35f)
val activeContentColor = colorScheme.onPrimary
val disabledContentColor = colorScheme.onPrimary.copy(alpha = 0.45f)
var showMorePopup by remember { mutableStateOf(false) }
Row(

View File

@@ -10,6 +10,7 @@ junit = "4.13.2"
androidxJunit = "1.3.0"
espressoCore = "3.7.0"
miuix = "0.9.0"
backdrop = "1.0.6"
material = "1.13.0"
runtime = "1.10.5"
androidxCorePip = "1.0.0-alpha02"
@@ -38,6 +39,7 @@ miuix-blur = { group = "top.yukonga.miuix.kmp", name = "miuix-blur", version.ref
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" }
backdrop = { group = "io.github.kyant0", name = "backdrop", version.ref = "backdrop" }
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
androidx-core-pip = { group = "androidx.core", name = "core-pip", version.ref = "androidxCorePip" }
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidxBiometric" }