refactor: UI refresh
- 仍然是抄的 KernelSU 管理器和 miuix example - TopAppBar 与 dock 栏模糊 - 液态玻璃(`com.kyant.backdrop`)悬浮 dock 栏
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -110,7 +110,6 @@ class StreamActivity : FragmentActivity() {
|
||||
//+ onPictureInPictureUiStateChanged
|
||||
//- onUserLeaveHint
|
||||
|
||||
// @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
|
||||
super.onPictureInPictureUiStateChanged(pipState)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
if (isDark) listOf(
|
||||
BlendColorEntry(Color(0xE6A1A1A1), BlurBlendMode.ColorDodge),
|
||||
BlendColorEntry(Color(0x4DE6E6E6), BlurBlendMode.LinearLight),
|
||||
BlendColorEntry(Color(0xFF1AF500), BlurBlendMode.Lab),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
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(
|
||||
if (isDark) listOf(
|
||||
BlendColorEntry(Color(0x4DA9A9A9), BlurBlendMode.Luminosity),
|
||||
BlendColorEntry(Color(0x1A9C9C9C), BlurBlendMode.PlusDarker),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
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);
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -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,15 +136,22 @@ 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 = {
|
||||
BlurredBar(backdrop = blurBackdrop) {
|
||||
TopAppBar(
|
||||
title = "设备",
|
||||
color =
|
||||
if (blurActive) Color.Transparent
|
||||
else colorScheme.surface,
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = { showThreePointMenu = true },
|
||||
@@ -168,21 +182,26 @@ fun DeviceTabScreen(
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
},
|
||||
) { pagePadding ->
|
||||
Box(modifier = if (blurActive) Modifier.layerBackdrop(blurBackdrop) else Modifier) {
|
||||
DeviceTabPage(
|
||||
contentPadding = pagePadding,
|
||||
scrollBehavior = scrollBehavior,
|
||||
scrcpy = scrcpy,
|
||||
bottomInnerPadding = bottomInnerPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,13 +339,15 @@ fun MainScreen() {
|
||||
|
||||
val rootEntryProvider = entryProvider<NavKey> {
|
||||
entry(RootScreen.Home) {
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
tabs.forEach { tab ->
|
||||
NavigationBarItem(
|
||||
selected = currentTab == tab,
|
||||
onClick = {
|
||||
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,
|
||||
@@ -332,19 +357,55 @@ fun MainScreen() {
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
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 ->
|
||||
val bottomInnerPadding: Dp = if (asBundle.floatingBottomBar) {
|
||||
12.dp + 64.dp + contentPadding.calculateBottomPadding()
|
||||
} else {
|
||||
contentPadding.calculateBottomPadding()
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(if (blurBackdrop != null) Modifier.miuixLayerBackdrop(blurBackdrop) else Modifier),
|
||||
) {
|
||||
HorizontalPager(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = contentPadding.calculateBottomPadding()),
|
||||
.then(
|
||||
if (asBundle.floatingBottomBar && floatingBarBlurActive) {
|
||||
Modifier.layerBackdrop(glassBackdrop)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
state = pagerState,
|
||||
beyondViewportPageCount = 1,
|
||||
) { page ->
|
||||
@@ -354,16 +415,69 @@ fun MainScreen() {
|
||||
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,
|
||||
@@ -371,6 +485,7 @@ fun MainScreen() {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entry(RootScreen.Advanced) {
|
||||
ScrcpyAllOptionsScreen(
|
||||
@@ -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,
|
||||
|
||||
@@ -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,8 +219,12 @@ internal fun ScrcpyAllOptionsScreen(
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
BlurredBar(backdrop = blurBackdrop) {
|
||||
TopAppBar(
|
||||
title = "所有参数",
|
||||
color =
|
||||
if (blurActive) Color.Transparent
|
||||
else colorScheme.surface,
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigator.pop) {
|
||||
Icon(
|
||||
@@ -269,12 +281,18 @@ internal fun ScrcpyAllOptionsScreen(
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
val snackbar = LocalSnackbarController.current
|
||||
SnackbarHost(snackbar.hostState)
|
||||
},
|
||||
) { contentPadding ->
|
||||
Box(
|
||||
modifier =
|
||||
if (blurActive) Modifier.layerBackdrop(blurBackdrop)
|
||||
else Modifier,
|
||||
) {
|
||||
ScrcpyAllOptionsPage(
|
||||
contentPadding = contentPadding,
|
||||
scrollBehavior = scrollBehavior,
|
||||
@@ -286,6 +304,7 @@ internal fun ScrcpyAllOptionsScreen(
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,37 +60,56 @@ 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 = {
|
||||
BlurredBar(backdrop = blurBackdrop) {
|
||||
TopAppBar(
|
||||
title = "设置",
|
||||
color =
|
||||
if (blurActive) Color.Transparent
|
||||
else colorScheme.surface,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
},
|
||||
) { pagePadding ->
|
||||
Box(
|
||||
modifier =
|
||||
if (blurActive) Modifier.layerBackdrop(blurBackdrop)
|
||||
else Modifier,
|
||||
) {
|
||||
SettingsPage(
|
||||
contentPadding = pagePadding,
|
||||
scrollBehavior = scrollBehavior,
|
||||
bottomInnerPadding = bottomInnerPadding,
|
||||
onOpenReorderDevices = onOpenReorderDevices,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,18 +40,26 @@ 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 = {
|
||||
BlurredBar(backdrop = blurBackdrop) {
|
||||
TopAppBar(
|
||||
title = "虚拟按钮排序",
|
||||
color =
|
||||
if (blurActive) Color.Transparent
|
||||
else colorScheme.surface,
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigator.pop) {
|
||||
Icon(
|
||||
@@ -58,21 +70,23 @@ internal fun VirtualButtonOrderScreen(
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
},
|
||||
) { pagePadding ->
|
||||
Box(modifier = if (blurActive) Modifier.layerBackdrop(blurBackdrop) else Modifier) {
|
||||
VirtualButtonOrderPage(
|
||||
contentPadding = pagePadding,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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)
|
||||
)
|
||||
},
|
||||
)()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
"""
|
||||
@@ -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);
|
||||
}
|
||||
"""
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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() })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user