refactor: UI refresh

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
package io.github.miuzarte.scrcpyforandroid.pages package io.github.miuzarte.scrcpyforandroid.pages
import android.content.Intent 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack 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.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.layout.positionInWindow
@@ -45,7 +47,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.net.toUri import androidx.core.net.toUri
import io.github.miuzarte.scrcpyforandroid.BuildConfig 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.services.AppUpdateChecker
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults 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.BlurColors
import top.yukonga.miuix.kmp.blur.BlurDefaults import top.yukonga.miuix.kmp.blur.BlurDefaults
import top.yukonga.miuix.kmp.blur.LayerBackdrop 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.isRenderEffectSupported
import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported
import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.blur.layerBackdrop
import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop
import top.yukonga.miuix.kmp.blur.textureBlur import top.yukonga.miuix.kmp.blur.textureBlur
import top.yukonga.miuix.kmp.preference.ArrowPreference import top.yukonga.miuix.kmp.preference.ArrowPreference
import top.yukonga.miuix.kmp.shapes.SmoothRoundedCornerShape import top.yukonga.miuix.kmp.shapes.SmoothRoundedCornerShape
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme 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 @Composable
internal fun AboutScreen() { internal fun AboutScreen() {
val navigator = LocalRootNavigator.current val navigator = LocalRootNavigator.current
val enableBlur = LocalEnableBlur.current
val blurBackdrop = rememberBlurBackdrop(enableBlur)
val topAppBarScrollBehavior = MiuixScrollBehavior() val topAppBarScrollBehavior = MiuixScrollBehavior()
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
var logoHeightPx by remember { mutableIntStateOf(0) } var logoHeightPx by remember { mutableIntStateOf(0) }
@@ -101,7 +104,12 @@ internal fun AboutScreen() {
SmallTopAppBar( SmallTopAppBar(
title = "关于", title = "关于",
scrollBehavior = topAppBarScrollBehavior, 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), titleColor = colorScheme.onSurface.copy(alpha = scrollProgress),
defaultWindowInsetsPadding = false, defaultWindowInsetsPadding = false,
navigationIcon = { navigationIcon = {
@@ -120,6 +128,7 @@ internal fun AboutScreen() {
top = innerPadding.calculateTopPadding(), top = innerPadding.calculateTopPadding(),
bottom = innerPadding.calculateBottomPadding(), bottom = innerPadding.calculateBottomPadding(),
), ),
enableBlur = enableBlur,
lazyListState = lazyListState, lazyListState = lazyListState,
scrollProgress = scrollProgress, scrollProgress = scrollProgress,
onLogoHeightChanged = { logoHeightPx = it }, onLogoHeightChanged = { logoHeightPx = it },
@@ -130,16 +139,20 @@ internal fun AboutScreen() {
@Composable @Composable
private fun AboutContent( private fun AboutContent(
padding: PaddingValues, padding: PaddingValues,
lazyListState: androidx.compose.foundation.lazy.LazyListState, enableBlur: Boolean,
lazyListState: LazyListState,
scrollProgress: Float, scrollProgress: Float,
onLogoHeightChanged: (Int) -> Unit, onLogoHeightChanged: (Int) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val updateState by AppUpdateChecker.state.collectAsState() val updateState by AppUpdateChecker.state.collectAsState()
val backdrop = rememberLayerBackdrop() val backdrop = rememberLayerBackdrop()
val blurEnabled = remember { isRenderEffectSupported() } var isOs3Effect by remember { mutableStateOf(true) }
val isDark = MiuixTheme.colorScheme.background.luminance() < 0.5f 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) } val cardBlendColors = remember(isDark) { aboutCardBlendToken(isDark) }
var logoAreaY by remember { mutableFloatStateOf(0f) } var logoAreaY by remember { mutableFloatStateOf(0f) }
var projectNameY by remember { mutableFloatStateOf(0f) } var projectNameY by remember { mutableFloatStateOf(0f) }
var versionCodeY by remember { mutableFloatStateOf(0f) } var versionCodeY by remember { mutableFloatStateOf(0f) }
@@ -172,12 +185,12 @@ private fun AboutContent(
val stage2TotalLength = versionCodeY - projectNameY val stage2TotalLength = versionCodeY - projectNameY
val versionCodeDelay = stage1TotalLength * 0.5f val versionCodeDelay = stage1TotalLength * 0.5f
versionCodeProgress = versionCodeProgress =
((offset.toFloat() - versionCodeDelay) / (stage1TotalLength - versionCodeDelay).coerceAtLeast( ((offset.toFloat() - versionCodeDelay) / (stage1TotalLength - versionCodeDelay)
1f .coerceAtLeast(1f))
))
.coerceIn(0f, 1f) .coerceIn(0f, 1f)
projectNameProgress = projectNameProgress =
((offset.toFloat() - stage1TotalLength) / stage2TotalLength.coerceAtLeast(1f)) ((offset.toFloat() - stage1TotalLength) / stage2TotalLength
.coerceAtLeast(1f))
.coerceIn(0f, 1f) .coerceIn(0f, 1f)
} }
.collect {} .collect {}
@@ -186,8 +199,11 @@ private fun AboutContent(
BgEffectBackground( BgEffectBackground(
dynamicBackground = true, dynamicBackground = true,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bgModifier = Modifier.layerBackdrop(backdrop), bgModifier =
if (blurEnabled) Modifier.layerBackdrop(backdrop)
else Modifier,
effectBackground = true, effectBackground = true,
isOs3Effect = isOs3Effect,
alpha = { 1f - scrollProgress }, alpha = { 1f - scrollProgress },
) { ) {
Column( Column(
@@ -206,12 +222,12 @@ private fun AboutContent(
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val textMeasurer = rememberTextMeasurer() val textMeasurer = rememberTextMeasurer()
val baseTitleFontSize = 35.sp val baseTitleFontSize = 32.sp
val titleLayout = remember(textMeasurer) { val titleLayout = remember(textMeasurer) {
textMeasurer.measure( textMeasurer.measure(
text = "Scrcpy for Android", text = "Scrcpy for Android",
style = TextStyle( style = TextStyle(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Black,
fontSize = baseTitleFontSize, fontSize = baseTitleFontSize,
), ),
softWrap = false, softWrap = false,
@@ -226,7 +242,7 @@ private fun AboutContent(
Text( Text(
text = "Scrcpy for Android", text = "Scrcpy for Android",
modifier = Modifier modifier = Modifier
.padding(top = 12.dp, bottom = 5.dp) .padding(top = 12.dp, bottom = 6.dp)
.onGloballyPositioned { coordinates -> .onGloballyPositioned { coordinates ->
if (projectNameY != 0f) return@onGloballyPositioned if (projectNameY != 0f) return@onGloballyPositioned
val y = coordinates.positionInWindow().y val y = coordinates.positionInWindow().y
@@ -243,13 +259,11 @@ private fun AboutContent(
shape = SmoothRoundedCornerShape(16.dp), shape = SmoothRoundedCornerShape(16.dp),
blurRadius = 150f, blurRadius = 150f,
noiseCoefficient = BlurDefaults.NoiseCoefficient, noiseCoefficient = BlurDefaults.NoiseCoefficient,
colors = BlurColors( colors = BlurColors(blendColors = heroBlendColors),
blendColors = heroBlendColors(isDark), contentBlendMode = BlendMode.DstIn,
),
contentBlendMode = ComposeBlendMode.DstIn,
enabled = blurEnabled, enabled = blurEnabled,
), ),
color = MiuixTheme.colorScheme.onBackground, color = colorScheme.onBackground,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = titleFontSize, fontSize = titleFontSize,
) )
@@ -269,7 +283,7 @@ private fun AboutContent(
val size = coordinates.size val size = coordinates.size
versionCodeY = y + size.height versionCodeY = y + size.height
}, },
color = MiuixTheme.colorScheme.onSurfaceVariantSummary, color = colorScheme.onSurfaceVariantSummary,
fontSize = 14.sp, fontSize = 14.sp,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
@@ -288,7 +302,7 @@ private fun AboutContent(
val size = coordinates.size val size = coordinates.size
versionCodeY = y + size.height versionCodeY = y + size.height
}, },
color = MiuixTheme.colorScheme.onSurfaceVariantSummary, color = colorScheme.onSurfaceVariantSummary,
fontSize = 14.sp, fontSize = 14.sp,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
@@ -309,6 +323,13 @@ private fun AboutContent(
.fillMaxWidth() .fillMaxWidth()
.height(320.dp) .height(320.dp)
.padding(top = 36.dp) .padding(top = 36.dp)
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
isOs3Effect = !isOs3Effect
}
)
}
.onSizeChanged { onLogoHeightChanged(it.height) } .onSizeChanged { onLogoHeightChanged(it.height) }
.onGloballyPositioned { coordinates -> .onGloballyPositioned { coordinates ->
val y = coordinates.positionInWindow().y val y = coordinates.positionInWindow().y
@@ -336,7 +357,7 @@ private fun AboutContent(
endActions = { endActions = {
Text( Text(
text = "GitHub", text = "GitHub",
fontSize = MiuixTheme.textStyles.body2.fontSize, fontSize = textStyles.body2.fontSize,
color = colorScheme.onSurfaceVariantActions, color = colorScheme.onSurfaceVariantActions,
) )
}, },
@@ -352,7 +373,7 @@ private fun AboutContent(
releaseStatusText?.let { releaseStatusText?.let {
Text( Text(
text = it, text = it,
fontSize = MiuixTheme.textStyles.body2.fontSize, fontSize = textStyles.body2.fontSize,
color = colorScheme.onSurfaceVariantActions, color = colorScheme.onSurfaceVariantActions,
) )
} }
@@ -375,7 +396,7 @@ private fun AboutContent(
endActions = { endActions = {
Text( Text(
text = "Apache-2.0", text = "Apache-2.0",
fontSize = MiuixTheme.textStyles.body2.fontSize, fontSize = textStyles.body2.fontSize,
color = colorScheme.onSurfaceVariantActions, color = colorScheme.onSurfaceVariantActions,
) )
}, },
@@ -413,335 +434,35 @@ private fun AboutCard(
enabled = blurEnabled, enabled = blurEnabled,
), ),
colors = CardDefaults.defaultColors( colors = CardDefaults.defaultColors(
if (blurEnabled) Color.Transparent else MiuixTheme.colorScheme.surfaceContainer, color =
Color.Transparent, if (blurEnabled) Color.Transparent
else colorScheme.surfaceContainer,
contentColor = Color.Transparent,
), ),
) { ) {
content() content()
} }
} }
@Composable
private fun heroBlendColors(isDark: Boolean): List<BlendColorEntry> = private fun heroBlendColors(isDark: Boolean): List<BlendColorEntry> =
remember(isDark) { if (isDark) listOf(
if (isDark) { BlendColorEntry(Color(0xE6A1A1A1), BlurBlendMode.ColorDodge),
listOf( BlendColorEntry(Color(0x4DE6E6E6), BlurBlendMode.LinearLight),
BlendColorEntry(Color(0xE6A1A1A1), BlurBlendMode.ColorDodge), BlendColorEntry(Color(0xFF1AF500), BlurBlendMode.Lab),
BlendColorEntry(Color(0x4DE6E6E6), BlurBlendMode.LinearLight), )
BlendColorEntry(Color(0xFF1AF500), BlurBlendMode.Lab), else listOf(
) BlendColorEntry(Color(0xCC4A4A4A), BlurBlendMode.ColorBurn),
} else { BlendColorEntry(Color(0xFF4F4F4F), BlurBlendMode.LinearLight),
listOf( BlendColorEntry(Color(0xFF1AF200), BlurBlendMode.Lab),
BlendColorEntry(Color(0xCC4A4A4A), BlurBlendMode.ColorBurn), )
BlendColorEntry(Color(0xFF4F4F4F), BlurBlendMode.LinearLight),
BlendColorEntry(Color(0xFF1AF200), BlurBlendMode.Lab),
)
}
}
private fun aboutCardBlendToken(isDark: Boolean): List<BlendColorEntry> = private fun aboutCardBlendToken(isDark: Boolean): List<BlendColorEntry> =
if (isDark) { if (isDark) listOf(
listOf( BlendColorEntry(Color(0x4DA9A9A9), BlurBlendMode.Luminosity),
BlendColorEntry(Color(0x4DA9A9A9), BlurBlendMode.Luminosity), BlendColorEntry(Color(0x1A9C9C9C), BlurBlendMode.PlusDarker),
BlendColorEntry(Color(0x1A9C9C9C), BlurBlendMode.PlusDarker), )
) else listOf(
} else { BlendColorEntry(Color(0x340034F9), BlurBlendMode.Overlay),
listOf( BlendColorEntry(Color(0xB3FFFFFF), BlurBlendMode.HardLight),
BlendColorEntry(Color(0x340034F9), BlurBlendMode.Overlay), )
BlendColorEntry(Color(0xB3FFFFFF), BlurBlendMode.HardLight),
)
}
@Composable
private inline fun BgEffectBackground(
dynamicBackground: Boolean,
modifier: Modifier = Modifier,
bgModifier: Modifier = Modifier,
effectBackground: Boolean = true,
crossinline alpha: () -> Float = { 1f },
content: @Composable BoxScope.() -> Unit,
) {
val shaderSupported = remember { isRuntimeShaderSupported() }
if (!shaderSupported) {
Box(modifier = modifier, content = content)
return
}
Box(modifier = modifier) {
val surface = colorScheme.surface
val painter = remember { BgEffectPainter() }
val animTime = rememberFrameTimeSeconds(dynamicBackground)
val isDark = colorScheme.background.luminance() < 0.5f
Canvas(
modifier = Modifier
.fillMaxSize()
.then(bgModifier),
) {
drawRect(surface)
if (effectBackground) {
val drawHeight = size.height * 0.78f
painter.updateResolution(size.width, size.height)
painter.updatePresetIfNeeded(drawHeight, size.height, size.width, isDark)
painter.updateAnimTime(animTime())
drawRect(painter.runtimeShader.asBrush(), alpha = alpha())
}
}
content()
}
}
@Composable
private fun rememberFrameTimeSeconds(
playing: Boolean = true,
): () -> Float {
var time by remember { mutableFloatStateOf(0f) }
var startOffset by remember { mutableFloatStateOf(0f) }
LaunchedEffect(playing) {
if (!playing) {
startOffset = time
return@LaunchedEffect
}
val start = withFrameNanos { it }
while (playing) {
val now = withFrameNanos { it }
time = startOffset + (now - start) / 1_000_000_000f
}
}
return { time }
}
// @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private class BgEffectPainter {
val runtimeShader by lazy {
RuntimeShader(OS2_BG_FRAG).also { initStaticUniforms(it) }
}
private val resolution = FloatArray(2)
private val bound = FloatArray(4)
private var animTime = Float.NaN
private var isDarkCached: Boolean? = null
private var presetApplied = false
private fun initStaticUniforms(shader: RuntimeShader) {
shader.setFloatUniform("uTranslateY", 0f)
shader.setFloatUniform("uNoiseScale", 1.5f)
shader.setFloatUniform("uPointOffset", 0.1f)
shader.setFloatUniform("uPointRadiusMulti", 1f)
shader.setFloatUniform("uAlphaMulti", 1f)
}
fun updateResolution(width: Float, height: Float) {
if (resolution[0] == width && resolution[1] == height) return
resolution[0] = width
resolution[1] = height
runtimeShader.setFloatUniform("uResolution", resolution)
}
fun updateAnimTime(time: Float) {
if (animTime == time) return
animTime = time
runtimeShader.setFloatUniform("uAnimTime", animTime)
}
fun updatePresetIfNeeded(
logoHeight: Float,
height: Float,
width: Float,
isDark: Boolean,
) {
if (presetApplied && isDarkCached == isDark) return
updateBound(logoHeight, height, width)
val preset = if (isDark) PHONE_DARK else PHONE_LIGHT
runtimeShader.setFloatUniform("uPoints", preset.points)
runtimeShader.setFloatUniform("uColors", preset.colors)
runtimeShader.setFloatUniform("uLightOffset", preset.lightOffset)
runtimeShader.setFloatUniform("uSaturateOffset", preset.saturateOffset)
runtimeShader.setFloatUniform("uBound", bound)
isDarkCached = isDark
presetApplied = true
}
private fun updateBound(
logoHeight: Float,
totalHeight: Float,
totalWidth: Float,
) {
val heightRatio = logoHeight / totalHeight
if (totalWidth <= totalHeight) {
bound[0] = 0f
bound[1] = 1f - heightRatio
bound[2] = 1f
bound[3] = heightRatio
} else {
val aspectRatio = totalWidth / totalHeight
val contentCenterY = 1f - heightRatio / 2f
bound[0] = 0f
bound[1] = contentCenterY - aspectRatio / 2f
bound[2] = 1f
bound[3] = aspectRatio
}
}
private data class BgEffectPreset(
val points: FloatArray,
val colors: FloatArray,
val lightOffset: Float,
val saturateOffset: Float,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BgEffectPreset
if (lightOffset != other.lightOffset) return false
if (saturateOffset != other.saturateOffset) return false
if (!points.contentEquals(other.points)) return false
if (!colors.contentEquals(other.colors)) return false
return true
}
override fun hashCode(): Int {
var result = lightOffset.hashCode()
result = 31 * result + saturateOffset.hashCode()
result = 31 * result + points.contentHashCode()
result = 31 * result + colors.contentHashCode()
return result
}
}
private companion object {
val PHONE_LIGHT = BgEffectPreset(
points = floatArrayOf(
0.67f, 0.42f, 1.0f, 0.69f, 0.75f, 1.0f,
0.14f, 0.71f, 0.95f, 0.14f, 0.27f, 0.8f,
),
colors = floatArrayOf(
0.57f, 0.76f, 0.98f, 1.0f,
0.98f, 0.85f, 0.68f, 1.0f,
0.98f, 0.75f, 0.93f, 1.0f,
0.73f, 0.70f, 0.98f, 1.0f,
),
lightOffset = 0.1f,
saturateOffset = 0.2f,
)
val PHONE_DARK = BgEffectPreset(
points = floatArrayOf(
0.63f, 0.50f, 0.88f, 0.69f, 0.75f, 0.80f,
0.17f, 0.66f, 0.81f, 0.14f, 0.24f, 0.72f,
),
colors = floatArrayOf(
0.0f, 0.31f, 0.58f, 1.0f,
0.53f, 0.29f, 0.15f, 1.0f,
0.46f, 0.06f, 0.27f, 1.0f,
0.16f, 0.12f, 0.45f, 1.0f,
),
lightOffset = -0.1f,
saturateOffset = 0.2f,
)
}
}
private const val OS2_BG_FRAG = """
uniform vec2 uResolution;
uniform shader uTex;
uniform float uAnimTime;
uniform vec4 uBound;
uniform float uTranslateY;
uniform vec3 uPoints[4];
uniform vec4 uColors[4];
uniform float uAlphaMulti;
uniform float uNoiseScale;
uniform float uPointOffset;
uniform float uPointRadiusMulti;
uniform float uSaturateOffset;
uniform float uLightOffset;
vec3 rgb2hsv(vec3 c)
{
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c)
{
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.13);
p3 += dot(p3, p3.yzx + 3.333);
return fract((p3.x + p3.y) * p3.z);
}
float perlin(vec2 x) {
vec2 i = floor(x);
vec2 f = fract(x);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
float gradientNoise(in vec2 uv)
{
return fract(52.9829189 * fract(dot(uv, vec2(0.06711056, 0.00583715))));
}
vec4 main(vec2 fragCoord){
vec2 vUv = fragCoord/uResolution;
vUv.y = 1.0-vUv.y;
vec2 uv = vUv;
uv -= vec2(0., uTranslateY);
uv.xy -= uBound.xy;
uv.xy /= uBound.zw;
vec3 hsv;
vec4 color = vec4(0.0);
float noiseValue = perlin(vUv * uNoiseScale + vec2(-uAnimTime, -uAnimTime));
for (int i = 0; i < 4; i++){
vec4 pointColor = uColors[i];
pointColor.rgb *= pointColor.a;
vec2 point = uPoints[i].xy;
float rad = uPoints[i].z * uPointRadiusMulti;
point.x += sin(uAnimTime + point.y) * uPointOffset;
point.y += cos(uAnimTime + point.x) * uPointOffset;
float d = distance(uv, point);
float pct = smoothstep(rad, 0., d);
color.rgb = mix(color.rgb, pointColor.rgb, pct);
color.a = mix(color.a, pointColor.a, pct);
}
float oppositeNoise = smoothstep(0., 1., noiseValue);
if (color.a > 0.001) {
color.rgb /= color.a;
} else {
color.rgb = vec3(0.0);
}
hsv = rgb2hsv(color.rgb);
hsv.y = mix(hsv.y, 0.0, oppositeNoise * uSaturateOffset);
color.rgb = hsv2rgb(hsv);
color.rgb += oppositeNoise * uLightOffset;
color.a = clamp(color.a, 0., 1.);
color.a *= uAlphaMulti;
vec4 uiColor = uTex.eval(vec2(vUv.x, 1.0 - vUv.y)*uResolution);
vec4 fragColor;
color.rgb += (10.0 / 255.0) * gradientNoise(fragCoord.xy) - (5.0 / 255.0);
if (uiColor.a < 0.01) {
fragColor = color;
} else {
fragColor = uiColor;
}
return vec4(fragColor.rgb*fragColor.a, fragColor.a);
}
"""

View File

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

View File

@@ -1,7 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.pages package io.github.miuzarte.scrcpyforandroid.pages
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.graphics.Rect import android.graphics.Rect
import android.util.Log import android.util.Log
import androidx.activity.compose.BackHandler 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.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,132 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
private val OS2_PHONE_LIGHT_COLORS = floatArrayOf(
0.57f, 0.76f, 0.98f, 1.0f,
0.98f, 0.85f, 0.68f, 1.0f,
0.98f, 0.75f, 0.93f, 1.0f,
0.73f, 0.70f, 0.98f, 1.0f,
)
private val OS2_PHONE_LIGHT = BgEffectConfig.Config(
points = floatArrayOf(
0.67f,
0.42f,
1.0f,
0.69f,
0.75f,
1.0f,
0.14f,
0.71f,
0.95f,
0.14f,
0.27f,
0.8f
),
colors1 = OS2_PHONE_LIGHT_COLORS,
colors2 = OS2_PHONE_LIGHT_COLORS,
colors3 = OS2_PHONE_LIGHT_COLORS,
colorInterpPeriod = 100f,
lightOffset = 0.1f,
saturateOffset = 0.2f,
pointOffset = 0.1f,
)
private val OS2_PHONE_DARK_COLORS = floatArrayOf(
0.0f, 0.31f, 0.58f, 1.0f,
0.53f, 0.29f, 0.15f, 1.0f,
0.46f, 0.06f, 0.27f, 1.0f,
0.16f, 0.12f, 0.45f, 1.0f,
)
private val OS2_PHONE_DARK = BgEffectConfig.Config(
points = floatArrayOf(
0.63f,
0.50f,
0.88f,
0.69f,
0.75f,
0.80f,
0.17f,
0.66f,
0.81f,
0.14f,
0.24f,
0.72f
),
colors1 = OS2_PHONE_DARK_COLORS,
colors2 = OS2_PHONE_DARK_COLORS,
colors3 = OS2_PHONE_DARK_COLORS,
colorInterpPeriod = 100f,
lightOffset = -0.1f,
saturateOffset = 0.2f,
pointOffset = 0.1f,
)
private val OS2_PAD_LIGHT_COLORS = floatArrayOf(
0.57f, 0.76f, 0.98f, 1.0f,
0.98f, 0.85f, 0.68f, 1.0f,
0.98f, 0.75f, 0.93f, 0.95f,
0.73f, 0.70f, 0.98f, 0.90f,
)
private val OS2_PAD_LIGHT = BgEffectConfig.Config(
points = floatArrayOf(
0.67f,
0.37f,
0.88f,
0.54f,
0.66f,
1.0f,
0.37f,
0.71f,
0.68f,
0.28f,
0.26f,
0.62f
),
colors1 = OS2_PAD_LIGHT_COLORS,
colors2 = OS2_PAD_LIGHT_COLORS,
colors3 = OS2_PAD_LIGHT_COLORS,
colorInterpPeriod = 100f,
lightOffset = 0.1f,
saturateOffset = 0f,
pointOffset = 0.1f,
)
private val OS2_PAD_DARK_COLORS = floatArrayOf(
0.0f, 0.31f, 0.58f, 1.0f,
0.53f, 0.29f, 0.15f, 1.0f,
0.46f, 0.06f, 0.27f, 1.0f,
0.16f, 0.12f, 0.45f, 1.0f,
)
private val OS2_PAD_DARK = BgEffectConfig.Config(
points = floatArrayOf(
0.55f,
0.42f,
1.0f,
0.56f,
0.75f,
1.0f,
0.40f,
0.59f,
0.71f,
0.43f,
0.09f,
0.75f
),
colors1 = OS2_PAD_DARK_COLORS,
colors2 = OS2_PAD_DARK_COLORS,
colors3 = OS2_PAD_DARK_COLORS,
colorInterpPeriod = 100f,
lightOffset = -0.1f,
saturateOffset = 0.2f,
pointOffset = 0.1f,
)
internal fun getOs2Config(deviceType: DeviceType, isDark: Boolean): BgEffectConfig.Config {
return when (deviceType) {
DeviceType.PHONE -> if (isDark) OS2_PHONE_DARK else OS2_PHONE_LIGHT
DeviceType.PAD -> if (isDark) OS2_PAD_DARK else OS2_PAD_LIGHT
}
}

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
@Composable
internal fun rememberFrameTimeSeconds(
playing: Boolean = true,
): () -> Float {
var time by remember { mutableFloatStateOf(0f) }
var startOffset by remember { mutableFloatStateOf(0f) }
LaunchedEffect(playing) {
if (!playing) {
startOffset = time
return@LaunchedEffect
}
val start = withFrameNanos { it }
while (playing) {
val now = withFrameNanos { it }
time = startOffset + (now - start) / 1_000_000_000f
}
}
return { time }
}

View File

@@ -0,0 +1,104 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
internal const val OS2_BG_FRAG = """
uniform vec2 uResolution;
uniform shader uTex;
uniform float uAnimTime;
uniform vec4 uBound;
uniform float uTranslateY;
uniform vec3 uPoints[4];
uniform vec4 uColors[4];
uniform float uAlphaMulti;
uniform float uNoiseScale;
uniform float uPointOffset;
uniform float uPointRadiusMulti;
uniform float uSaturateOffset;
uniform float uLightOffset;
vec3 rgb2hsv(vec3 c)
{
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c)
{
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.13);
p3 += dot(p3, p3.yzx + 3.333);
return fract((p3.x + p3.y) * p3.z);
}
float perlin(vec2 x) {
vec2 i = floor(x);
vec2 f = fract(x);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
float gradientNoise(in vec2 uv)
{
return fract(52.9829189 * fract(dot(uv, vec2(0.06711056, 0.00583715))));
}
vec4 main(vec2 fragCoord){
vec2 vUv = fragCoord/uResolution;
vUv.y = 1.0-vUv.y;
vec2 uv = vUv;
uv -= vec2(0., uTranslateY);
uv.xy -= uBound.xy;
uv.xy /= uBound.zw;
vec3 hsv;
vec4 color = vec4(0.0);
float noiseValue = perlin(vUv * uNoiseScale + vec2(-uAnimTime, -uAnimTime));
for (int i = 0; i < 4; i++){
vec4 pointColor = uColors[i];
pointColor.rgb *= pointColor.a;
vec2 point = uPoints[i].xy;
float rad = uPoints[i].z * uPointRadiusMulti;
point.x += sin(uAnimTime + point.y) * uPointOffset;
point.y += cos(uAnimTime + point.x) * uPointOffset;
float d = distance(uv, point);
float pct = smoothstep(rad, 0., d);
color.rgb = mix(color.rgb, pointColor.rgb, pct);
color.a = mix(color.a, pointColor.a, pct);
}
float oppositeNoise = smoothstep(0., 1., noiseValue);
if (color.a > 0.001) {
color.rgb /= color.a;
} else {
color.rgb = vec3(0.0);
}
hsv = rgb2hsv(color.rgb);
hsv.y = mix(hsv.y, 0.0, oppositeNoise * uSaturateOffset);
color.rgb = hsv2rgb(hsv);
color.rgb += oppositeNoise * uLightOffset;
color.a = clamp(color.a, 0., 1.);
color.a *= uAlphaMulti;
vec4 uiColor = uTex.eval(vec2(vUv.x, 1.0 - vUv.y)*uResolution);
vec4 fragColor;
color.rgb += (10.0 / 255.0) * gradientNoise(fragCoord.xy) - (5.0 / 255.0);
if (uiColor.a < 0.01) {
fragColor = color;
} else {
fragColor = uiColor;
}
return vec4(fragColor.rgb*fragColor.a, fragColor.a);
}
"""

View File

@@ -0,0 +1,140 @@
package io.github.miuzarte.scrcpyforandroid.pages.effect
internal const val OS3_BG_FRAG = """
uniform vec2 uResolution;
uniform shader uTex;
uniform shader uTexBitmap;
uniform vec2 uTexWH;
uniform float uAnimTime;
uniform vec4 uBound;
uniform float uTranslateY;
uniform vec3 uPoints[4];
uniform vec4 uColors[4];
uniform float uAlphaMulti;
uniform float uNoiseScale;
uniform float uPointOffset;
uniform float uPointRadiusMulti;
uniform float uSaturateOffset;
uniform float uLightOffset;
uniform float uAlphaOffset;
uniform float uShadowColorMulti;
uniform float uShadowColorOffset;
uniform float uShadowNoiseScale;
uniform float uShadowOffset;
vec3 hsl2rgb(in vec3 c) {
vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0, 4.0, 2.0), 6.0)-3.0)-1.0, 0.0, 1.0);
return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0));
}
vec3 HueShift (in vec3 Color, in float Shift) {
vec3 P = vec3(0.55735)*dot(vec3(0.55735), Color);
vec3 U = Color-P;
vec3 V = cross(vec3(0.55735), U);
Color = U*cos(Shift*6.2832) + V*sin(Shift*6.2832) + P;
return vec3(Color);
}
vec3 rgb2hsl(in vec3 c){
float h = 0.0; float s = 0.0; float l = 0.0;
float r = c.r; float g = c.g; float b = c.b;
float cMin = min(r, min(g, b));
float cMax = max(r, max(g, b));
l = (cMax + cMin) / 2.0;
if (cMax > cMin) {
float cDelta = cMax - cMin;
s = l < .0 ? cDelta / (cMax + cMin) : cDelta / (2.0 - (cMax + cMin));
if (r == cMax) h = (g - b) / cDelta;
else if (g == cMax) h = 2.0 + (b - r) / cDelta;
else h = 4.0 + (r - g) / cDelta;
if (h < 0.0) h += 6.0;
h = h / 6.0;
}
return vec3(h, s, l);
}
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.13);
p3 += dot(p3, p3.yzx + 3.333);
return fract((p3.x + p3.y) * p3.z);
}
float perlin(vec2 x) {
vec2 i = floor(x); vec2 f = fract(x);
float a = hash(i); float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}
float gradientNoise(in vec2 uv) {
return fract(52.9829189 * fract(dot(uv, vec2(0.06711056, 0.00583715))));
}
vec4 main(vec2 fragCoord){
vec2 vUv = fragCoord/uResolution;
vUv.y = 1.0-vUv.y;
vec2 uv = vUv;
uv -= vec2(0., uTranslateY);
uv.xy -= uBound.xy;
uv.xy /= uBound.zw;
vec3 hsv;
vec4 color = vec4(0.0);
float noiseValue = perlin(vUv * uNoiseScale + vec2(-uAnimTime, -uAnimTime));
for (int i = 0; i < 4; i++){
vec4 pointColor = uColors[i];
pointColor.rgb *= pointColor.a;
vec2 point = uPoints[i].xy;
float rad = uPoints[i].z * uPointRadiusMulti;
point.x += sin(uAnimTime + point.y) * uPointOffset;
point.y += cos(uAnimTime + point.x) * uPointOffset;
float d = distance(uv, point);
float pct = smoothstep(rad, 0., d);
color.rgb = mix(color.rgb, pointColor.rgb, pct);
color.a = mix(color.a, pointColor.a, pct);
}
float oppositeNoise = smoothstep(0., 1., noiseValue);
color.rgb /= color.a;
hsv = rgb2hsv(color.rgb);
hsv.y = mix(hsv.y, 0.0, oppositeNoise * uSaturateOffset);
color.rgb = hsv2rgb(hsv);
color.rgb += oppositeNoise * uLightOffset;
color.a = clamp(color.a, 0., 1.);
color.a *= uAlphaMulti;
vec4 uiColor = uTex.eval(vec2(vUv.x, 1.0 - vUv.y)*uResolution);
vec4 fragColor;
if (uiColor.a < 0.01) {
fragColor = color;
} else {
fragColor = uiColor;
}
color += (1.0 / 255.0) * gradientNoise(fragCoord.xy) - (0.5 / 255.0);
return vec4(color.rgb*color.a, color.a);
}
"""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,15 +5,10 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter 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 import kotlinx.coroutines.runBlocking
// not working in MIUI // not working in MIUI
class PictureInPictureActionReceiver : BroadcastReceiver() { class PictureInPictureActionReceiver : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action != ACTION_STOP_SCRCPY) return if (intent?.action != ACTION_STOP_SCRCPY) return
val pendingResult = goAsync() val pendingResult = goAsync()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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