From 0f4a82d8d5106bc56c0da43c0615dfa5bd3cb86c Mon Sep 17 00:00:00 2001 From: Miuzarte <982809597@qq.com> Date: Tue, 14 Apr 2026 16:54:04 +0800 Subject: [PATCH] refactor: UI refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 仍然是抄的 KernelSU 管理器和 miuix example - TopAppBar 与 dock 栏模糊 - 液态玻璃(`com.kyant.backdrop`)悬浮 dock 栏 --- app/build.gradle.kts | 5 +- .../LockscreenPasswordActivity.kt | 26 +- .../miuzarte/scrcpyforandroid/MainActivity.kt | 5 +- .../scrcpyforandroid/StreamActivity.kt | 1 - .../nativecore/AdbMdnsDiscoverer.kt | 6 +- .../nativecore/DirectAdbPairingClient.kt | 3 - .../scrcpyforandroid/pages/AboutScreen.kt | 425 +++--------------- .../scrcpyforandroid/pages/DeviceTabScreen.kt | 108 +++-- .../pages/FullscreenControlScreen.kt | 2 - .../scrcpyforandroid/pages/MainScreen.kt | 211 +++++++-- .../pages/ScrcpyAllOptionsScreen.kt | 160 ++++--- .../scrcpyforandroid/pages/SettingsScreen.kt | 95 +++- .../scrcpyforandroid/pages/StreamScreen.kt | 5 +- .../pages/VirtualButtonOrderScreen.kt | 79 ++-- .../pages/effect/BgEffectBackground.kt | 105 +++++ .../pages/effect/BgEffectConfig.kt | 36 ++ .../pages/effect/BgEffectConfigOs2.kt | 132 ++++++ .../pages/effect/BgEffectConfigOs3.kt | 256 +++++++++++ .../pages/effect/BgEffectPainter.kt | 127 ++++++ .../pages/effect/FrameTime.kt | 31 ++ .../pages/effect/OS2BgFrag.kt | 104 +++++ .../pages/effect/OS3BgFrag.kt | 140 ++++++ .../password/BiometricGate.kt | 7 +- .../password/PasswordPickerPopup.kt | 6 +- .../password/PasswordRepository.kt | 1 - .../scrcpyforandroid/scaffolds/LazyColumn.kt | 28 +- .../scrcpyforandroid/scaffolds/SuperSlider.kt | 7 +- .../scaffolds/SuperTextField.kt | 16 +- .../scrcpyforandroid/services/AppRuntime.kt | 3 - .../PictureInPictureActionReceiver.kt | 5 - .../scrcpyforandroid/storage/AppSettings.kt | 32 ++ .../scrcpyforandroid/ui/ThemePreferences.kt | 52 +++ .../ui/component/FloatingBottomBar.kt | 379 ++++++++++++++++ .../miuix/animation/DampedDragAnimation.kt | 141 ++++++ .../miuix/animation/InteractiveHighlight.kt | 105 +++++ .../miuix/modifier/DragGestureInspector.kt | 78 ++++ .../scrcpyforandroid/widgets/DeviceWidgets.kt | 43 +- .../widgets/ReorderableList.kt | 16 +- .../widgets/StatusCardModels.kt | 8 +- .../widgets/VirtualButtons.kt | 10 +- gradle/libs.versions.toml | 2 + 41 files changed, 2340 insertions(+), 661 deletions(-) create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectBackground.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfig.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfigOs2.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfigOs3.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectPainter.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/FrameTime.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/OS2BgFrag.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/OS3BgFrag.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/ThemePreferences.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/FloatingBottomBar.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/animation/DampedDragAnimation.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/animation/InteractiveHighlight.kt create mode 100644 app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/modifier/DragGestureInspector.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3de4cea..9fb89f2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -40,8 +40,8 @@ android { applicationId = "io.github.miuzarte.scrcpyforandroid" minSdk = 26 targetSdk = 36 - versionCode = 11 - versionName = "0.1.5" + versionCode = 12 + versionName = "0.1.6" externalNativeBuild { cmake { @@ -114,6 +114,7 @@ dependencies { implementation(libs.miuix.preference) implementation(libs.miuix.icons) implementation(libs.miuix.navigation3.ui) + implementation(libs.backdrop) implementation("io.github.vvb2060.ndk:boringssl:20250114") implementation("org.lsposed.libcxx:libcxx:27.0.12077973") implementation("org.bouncycastle:bcpkix-jdk18on:1.80") diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/LockscreenPasswordActivity.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/LockscreenPasswordActivity.kt index 337ff29..56c4cbb 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/LockscreenPasswordActivity.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/LockscreenPasswordActivity.kt @@ -36,6 +36,7 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -55,6 +56,7 @@ import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController import io.github.miuzarte.scrcpyforandroid.services.SnackbarController import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings +import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -80,6 +82,7 @@ import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Close import top.yukonga.miuix.kmp.icon.extended.Ok @@ -90,6 +93,8 @@ import top.yukonga.miuix.kmp.preference.ArrowPreference import top.yukonga.miuix.kmp.preference.SwitchPreference import top.yukonga.miuix.kmp.theme.ColorSchemeMode import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles import top.yukonga.miuix.kmp.theme.ThemeController class LockscreenPasswordActivity : FragmentActivity() { @@ -114,7 +119,10 @@ class LockscreenPasswordActivity : FragmentActivity() { remember(themeMode) { ThemeController(colorSchemeMode = themeMode) } val haptics = rememberAppHaptics() - MiuixTheme(controller = themeController) { + MiuixTheme( + controller = themeController, + smoothRounding = asBundle.smoothCorner, + ) { CompositionLocalProvider( LocalSnackbarController provides snackbarController, LocalAppHaptics provides haptics, @@ -213,10 +221,18 @@ private fun LockscreenPasswordScreen( pendingCreate = false } + val blurBackdrop = rememberBlurBackdrop(asBundle.blur) + Scaffold( topBar = { TopAppBar( title = "锁屏密码自动填充", + modifier = + if (blurBackdrop != null) Modifier.layerBackdrop(blurBackdrop) + else Modifier, + color = + if (blurBackdrop != null) Color.Transparent + else colorScheme.surface, navigationIcon = { IconButton(onClick = { activity.onBackPressedDispatcher.onBackPressed() }) { Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "返回") @@ -442,6 +458,7 @@ private fun LockscreenPasswordPage( LazyColumn( contentPadding = contentPadding, scrollBehavior = scrollBehavior, + bottomInnerPadding = UiSpacing.PageBottom, ) { item { Card { @@ -514,16 +531,14 @@ private fun LockscreenPasswordPage( "\n2. 在 root / posed / hook / 调试器 / 恶意输入法 等环境下,密码仍可能泄露" + "\n3. 本功能不会绕过系统锁屏认证,仅用于你已合法授权控制的设备" + "\n4. 关闭“填充密码时需要验证”会显著降低安全性,请谨慎选择", - fontSize = MiuixTheme.textStyles.body2.fontSize, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + fontSize = textStyles.body2.fontSize, + color = colorScheme.onSurfaceVariantSummary, modifier = Modifier .fillMaxWidth() .padding(top = UiSpacing.Large) .padding(horizontal = UiSpacing.Large), ) } - - item { Spacer(Modifier.height(UiSpacing.PageBottom)) } } } @@ -538,7 +553,6 @@ private fun PasswordMenuPopup( popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, alignment = PopupPositionProvider.Align.TopEnd, onDismissRequest = onDismissRequest, - enableWindowDim = false, ) { ListPopupColumn { SpinnerItemImpl( diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/MainActivity.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/MainActivity.kt index 21f896e..a451247 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/MainActivity.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/MainActivity.kt @@ -1,10 +1,9 @@ package io.github.miuzarte.scrcpyforandroid -import android.os.Build +import android.annotation.SuppressLint import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.annotation.RequiresApi import androidx.fragment.app.FragmentActivity import io.github.miuzarte.scrcpyforandroid.pages.MainScreen import io.github.miuzarte.scrcpyforandroid.password.BiometricGate @@ -13,12 +12,10 @@ import io.github.miuzarte.scrcpyforandroid.password.hasAuthenticatedOrigin import io.github.miuzarte.scrcpyforandroid.services.AppRuntime import io.github.miuzarte.scrcpyforandroid.services.AppWakeLocks import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration -import io.github.miuzarte.scrcpyforandroid.storage.Storage import kotlinx.coroutines.runBlocking // 生物认证需要 FragmentActivity class MainActivity : FragmentActivity() { - @RequiresApi(Build.VERSION_CODES.R) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/StreamActivity.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/StreamActivity.kt index fb39695..f30e483 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/StreamActivity.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/StreamActivity.kt @@ -110,7 +110,6 @@ class StreamActivity : FragmentActivity() { //+ onPictureInPictureUiStateChanged //- onUserLeaveHint - // @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) { super.onPictureInPictureUiStateChanged(pipState) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AdbMdnsDiscoverer.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AdbMdnsDiscoverer.kt index 2c0d1bd..1a8b9e2 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AdbMdnsDiscoverer.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/AdbMdnsDiscoverer.kt @@ -1,10 +1,9 @@ package io.github.miuzarte.scrcpyforandroid.nativecore +import android.content.Context import android.net.nsd.NsdManager import android.net.nsd.NsdServiceInfo -import android.os.Build import android.util.Log -import androidx.annotation.RequiresApi import java.io.IOException import java.net.InetSocketAddress import java.net.NetworkInterface @@ -15,7 +14,6 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference -@RequiresApi(Build.VERSION_CODES.R) /** * Performs mDNS discovery for ADB TLS pairing/connect services on the local network. * @@ -26,7 +24,7 @@ internal object AdbMdnsDiscoverer { private lateinit var nsdManager: NsdManager - fun init(context: android.content.Context) { + fun init(context: Context) { if (::nsdManager.isInitialized) return nsdManager = context.applicationContext.getSystemService(NsdManager::class.java) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbPairingClient.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbPairingClient.kt index 4cd8c9f..0cfbf05 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbPairingClient.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/DirectAdbPairingClient.kt @@ -1,8 +1,6 @@ package io.github.miuzarte.scrcpyforandroid.nativecore -import android.os.Build import android.util.Log -import androidx.annotation.RequiresApi import org.conscrypt.Conscrypt import java.io.Closeable import java.io.DataInputStream @@ -131,7 +129,6 @@ private class PairingContext private constructor(private val nativePtr: Long) { * keying material using the user-supplied pairing code, and exchanges peer * information to complete pairing. */ -@RequiresApi(Build.VERSION_CODES.R) internal class DirectAdbPairingClient( private val host: String, private val port: Int, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt index 5bb8937..e352d7b 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt @@ -1,10 +1,9 @@ package io.github.miuzarte.scrcpyforandroid.pages import android.content.Intent -import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -13,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack @@ -23,15 +23,17 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow -import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow @@ -45,7 +47,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.net.toUri import io.github.miuzarte.scrcpyforandroid.BuildConfig +import io.github.miuzarte.scrcpyforandroid.pages.effect.BgEffectBackground import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker +import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur +import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop import kotlinx.coroutines.flow.onEach import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.CardDefaults @@ -60,22 +65,20 @@ import top.yukonga.miuix.kmp.blur.BlurBlendMode import top.yukonga.miuix.kmp.blur.BlurColors import top.yukonga.miuix.kmp.blur.BlurDefaults import top.yukonga.miuix.kmp.blur.LayerBackdrop -import top.yukonga.miuix.kmp.blur.RuntimeShader -import top.yukonga.miuix.kmp.blur.asBrush import top.yukonga.miuix.kmp.blur.isRenderEffectSupported -import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop import top.yukonga.miuix.kmp.blur.textureBlur import top.yukonga.miuix.kmp.preference.ArrowPreference import top.yukonga.miuix.kmp.shapes.SmoothRoundedCornerShape -import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme -import androidx.compose.ui.graphics.BlendMode as ComposeBlendMode +import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles @Composable internal fun AboutScreen() { val navigator = LocalRootNavigator.current + val enableBlur = LocalEnableBlur.current + val blurBackdrop = rememberBlurBackdrop(enableBlur) val topAppBarScrollBehavior = MiuixScrollBehavior() val lazyListState = rememberLazyListState() var logoHeightPx by remember { mutableIntStateOf(0) } @@ -101,7 +104,12 @@ internal fun AboutScreen() { SmallTopAppBar( title = "关于", scrollBehavior = topAppBarScrollBehavior, - color = colorScheme.surface.copy(alpha = if (scrollProgress == 1f) 1f else 0f), + modifier = + if (blurBackdrop != null) Modifier.layerBackdrop(blurBackdrop) + else Modifier, + color = + if (blurBackdrop != null) Color.Transparent + else colorScheme.surface.copy(alpha = if (scrollProgress == 1f) 1f else 0f), titleColor = colorScheme.onSurface.copy(alpha = scrollProgress), defaultWindowInsetsPadding = false, navigationIcon = { @@ -120,6 +128,7 @@ internal fun AboutScreen() { top = innerPadding.calculateTopPadding(), bottom = innerPadding.calculateBottomPadding(), ), + enableBlur = enableBlur, lazyListState = lazyListState, scrollProgress = scrollProgress, onLogoHeightChanged = { logoHeightPx = it }, @@ -130,16 +139,20 @@ internal fun AboutScreen() { @Composable private fun AboutContent( padding: PaddingValues, - lazyListState: androidx.compose.foundation.lazy.LazyListState, + enableBlur: Boolean, + lazyListState: LazyListState, scrollProgress: Float, onLogoHeightChanged: (Int) -> Unit, ) { val context = LocalContext.current val updateState by AppUpdateChecker.state.collectAsState() val backdrop = rememberLayerBackdrop() - val blurEnabled = remember { isRenderEffectSupported() } - val isDark = MiuixTheme.colorScheme.background.luminance() < 0.5f + var isOs3Effect by remember { mutableStateOf(true) } + val blurEnabled = remember(enableBlur) { enableBlur && isRenderEffectSupported() } + val isDark = colorScheme.background.luminance() < 0.5f + val heroBlendColors = remember(isDark) { heroBlendColors(isDark) } val cardBlendColors = remember(isDark) { aboutCardBlendToken(isDark) } + var logoAreaY by remember { mutableFloatStateOf(0f) } var projectNameY by remember { mutableFloatStateOf(0f) } var versionCodeY by remember { mutableFloatStateOf(0f) } @@ -172,12 +185,12 @@ private fun AboutContent( val stage2TotalLength = versionCodeY - projectNameY val versionCodeDelay = stage1TotalLength * 0.5f versionCodeProgress = - ((offset.toFloat() - versionCodeDelay) / (stage1TotalLength - versionCodeDelay).coerceAtLeast( - 1f - )) + ((offset.toFloat() - versionCodeDelay) / (stage1TotalLength - versionCodeDelay) + .coerceAtLeast(1f)) .coerceIn(0f, 1f) projectNameProgress = - ((offset.toFloat() - stage1TotalLength) / stage2TotalLength.coerceAtLeast(1f)) + ((offset.toFloat() - stage1TotalLength) / stage2TotalLength + .coerceAtLeast(1f)) .coerceIn(0f, 1f) } .collect {} @@ -186,8 +199,11 @@ private fun AboutContent( BgEffectBackground( dynamicBackground = true, modifier = Modifier.fillMaxSize(), - bgModifier = Modifier.layerBackdrop(backdrop), + bgModifier = + if (blurEnabled) Modifier.layerBackdrop(backdrop) + else Modifier, effectBackground = true, + isOs3Effect = isOs3Effect, alpha = { 1f - scrollProgress }, ) { Column( @@ -206,12 +222,12 @@ private fun AboutContent( ) { val density = LocalDensity.current val textMeasurer = rememberTextMeasurer() - val baseTitleFontSize = 35.sp + val baseTitleFontSize = 32.sp val titleLayout = remember(textMeasurer) { textMeasurer.measure( text = "Scrcpy for Android", style = TextStyle( - fontWeight = FontWeight.Bold, + fontWeight = FontWeight.Black, fontSize = baseTitleFontSize, ), softWrap = false, @@ -226,7 +242,7 @@ private fun AboutContent( Text( text = "Scrcpy for Android", modifier = Modifier - .padding(top = 12.dp, bottom = 5.dp) + .padding(top = 12.dp, bottom = 6.dp) .onGloballyPositioned { coordinates -> if (projectNameY != 0f) return@onGloballyPositioned val y = coordinates.positionInWindow().y @@ -243,13 +259,11 @@ private fun AboutContent( shape = SmoothRoundedCornerShape(16.dp), blurRadius = 150f, noiseCoefficient = BlurDefaults.NoiseCoefficient, - colors = BlurColors( - blendColors = heroBlendColors(isDark), - ), - contentBlendMode = ComposeBlendMode.DstIn, + colors = BlurColors(blendColors = heroBlendColors), + contentBlendMode = BlendMode.DstIn, enabled = blurEnabled, ), - color = MiuixTheme.colorScheme.onBackground, + color = colorScheme.onBackground, fontWeight = FontWeight.Bold, fontSize = titleFontSize, ) @@ -269,7 +283,7 @@ private fun AboutContent( val size = coordinates.size versionCodeY = y + size.height }, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + color = colorScheme.onSurfaceVariantSummary, fontSize = 14.sp, textAlign = TextAlign.Center, ) @@ -288,7 +302,7 @@ private fun AboutContent( val size = coordinates.size versionCodeY = y + size.height }, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + color = colorScheme.onSurfaceVariantSummary, fontSize = 14.sp, textAlign = TextAlign.Center, ) @@ -309,6 +323,13 @@ private fun AboutContent( .fillMaxWidth() .height(320.dp) .padding(top = 36.dp) + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { + isOs3Effect = !isOs3Effect + } + ) + } .onSizeChanged { onLogoHeightChanged(it.height) } .onGloballyPositioned { coordinates -> val y = coordinates.positionInWindow().y @@ -336,7 +357,7 @@ private fun AboutContent( endActions = { Text( text = "GitHub", - fontSize = MiuixTheme.textStyles.body2.fontSize, + fontSize = textStyles.body2.fontSize, color = colorScheme.onSurfaceVariantActions, ) }, @@ -352,7 +373,7 @@ private fun AboutContent( releaseStatusText?.let { Text( text = it, - fontSize = MiuixTheme.textStyles.body2.fontSize, + fontSize = textStyles.body2.fontSize, color = colorScheme.onSurfaceVariantActions, ) } @@ -375,7 +396,7 @@ private fun AboutContent( endActions = { Text( text = "Apache-2.0", - fontSize = MiuixTheme.textStyles.body2.fontSize, + fontSize = textStyles.body2.fontSize, color = colorScheme.onSurfaceVariantActions, ) }, @@ -413,335 +434,35 @@ private fun AboutCard( enabled = blurEnabled, ), colors = CardDefaults.defaultColors( - if (blurEnabled) Color.Transparent else MiuixTheme.colorScheme.surfaceContainer, - Color.Transparent, + color = + if (blurEnabled) Color.Transparent + else colorScheme.surfaceContainer, + contentColor = Color.Transparent, ), ) { content() } } -@Composable private fun heroBlendColors(isDark: Boolean): List = - remember(isDark) { - if (isDark) { - listOf( - BlendColorEntry(Color(0xE6A1A1A1), BlurBlendMode.ColorDodge), - BlendColorEntry(Color(0x4DE6E6E6), BlurBlendMode.LinearLight), - BlendColorEntry(Color(0xFF1AF500), BlurBlendMode.Lab), - ) - } else { - listOf( - BlendColorEntry(Color(0xCC4A4A4A), BlurBlendMode.ColorBurn), - BlendColorEntry(Color(0xFF4F4F4F), BlurBlendMode.LinearLight), - BlendColorEntry(Color(0xFF1AF200), BlurBlendMode.Lab), - ) - } - } + if (isDark) listOf( + BlendColorEntry(Color(0xE6A1A1A1), BlurBlendMode.ColorDodge), + BlendColorEntry(Color(0x4DE6E6E6), BlurBlendMode.LinearLight), + BlendColorEntry(Color(0xFF1AF500), BlurBlendMode.Lab), + ) + else listOf( + BlendColorEntry(Color(0xCC4A4A4A), BlurBlendMode.ColorBurn), + BlendColorEntry(Color(0xFF4F4F4F), BlurBlendMode.LinearLight), + BlendColorEntry(Color(0xFF1AF200), BlurBlendMode.Lab), + ) private fun aboutCardBlendToken(isDark: Boolean): List = - if (isDark) { - listOf( - BlendColorEntry(Color(0x4DA9A9A9), BlurBlendMode.Luminosity), - BlendColorEntry(Color(0x1A9C9C9C), BlurBlendMode.PlusDarker), - ) - } else { - listOf( - BlendColorEntry(Color(0x340034F9), BlurBlendMode.Overlay), - BlendColorEntry(Color(0xB3FFFFFF), BlurBlendMode.HardLight), - ) - } + if (isDark) listOf( + BlendColorEntry(Color(0x4DA9A9A9), BlurBlendMode.Luminosity), + BlendColorEntry(Color(0x1A9C9C9C), BlurBlendMode.PlusDarker), + ) + else listOf( + BlendColorEntry(Color(0x340034F9), BlurBlendMode.Overlay), + BlendColorEntry(Color(0xB3FFFFFF), BlurBlendMode.HardLight), + ) -@Composable -private inline fun BgEffectBackground( - dynamicBackground: Boolean, - modifier: Modifier = Modifier, - bgModifier: Modifier = Modifier, - effectBackground: Boolean = true, - crossinline alpha: () -> Float = { 1f }, - content: @Composable BoxScope.() -> Unit, -) { - val shaderSupported = remember { isRuntimeShaderSupported() } - if (!shaderSupported) { - Box(modifier = modifier, content = content) - return - } - Box(modifier = modifier) { - val surface = colorScheme.surface - val painter = remember { BgEffectPainter() } - val animTime = rememberFrameTimeSeconds(dynamicBackground) - val isDark = colorScheme.background.luminance() < 0.5f - - Canvas( - modifier = Modifier - .fillMaxSize() - .then(bgModifier), - ) { - drawRect(surface) - if (effectBackground) { - val drawHeight = size.height * 0.78f - painter.updateResolution(size.width, size.height) - painter.updatePresetIfNeeded(drawHeight, size.height, size.width, isDark) - painter.updateAnimTime(animTime()) - drawRect(painter.runtimeShader.asBrush(), alpha = alpha()) - } - } - content() - } -} - -@Composable -private fun rememberFrameTimeSeconds( - playing: Boolean = true, -): () -> Float { - var time by remember { mutableFloatStateOf(0f) } - var startOffset by remember { mutableFloatStateOf(0f) } - - LaunchedEffect(playing) { - if (!playing) { - startOffset = time - return@LaunchedEffect - } - val start = withFrameNanos { it } - while (playing) { - val now = withFrameNanos { it } - time = startOffset + (now - start) / 1_000_000_000f - } - } - return { time } -} - -// @RequiresApi(Build.VERSION_CODES.TIRAMISU) -private class BgEffectPainter { - val runtimeShader by lazy { - RuntimeShader(OS2_BG_FRAG).also { initStaticUniforms(it) } - } - private val resolution = FloatArray(2) - private val bound = FloatArray(4) - private var animTime = Float.NaN - private var isDarkCached: Boolean? = null - private var presetApplied = false - - private fun initStaticUniforms(shader: RuntimeShader) { - shader.setFloatUniform("uTranslateY", 0f) - shader.setFloatUniform("uNoiseScale", 1.5f) - shader.setFloatUniform("uPointOffset", 0.1f) - shader.setFloatUniform("uPointRadiusMulti", 1f) - shader.setFloatUniform("uAlphaMulti", 1f) - } - - fun updateResolution(width: Float, height: Float) { - if (resolution[0] == width && resolution[1] == height) return - resolution[0] = width - resolution[1] = height - runtimeShader.setFloatUniform("uResolution", resolution) - } - - fun updateAnimTime(time: Float) { - if (animTime == time) return - animTime = time - runtimeShader.setFloatUniform("uAnimTime", animTime) - } - - fun updatePresetIfNeeded( - logoHeight: Float, - height: Float, - width: Float, - isDark: Boolean, - ) { - if (presetApplied && isDarkCached == isDark) return - updateBound(logoHeight, height, width) - val preset = if (isDark) PHONE_DARK else PHONE_LIGHT - runtimeShader.setFloatUniform("uPoints", preset.points) - runtimeShader.setFloatUniform("uColors", preset.colors) - runtimeShader.setFloatUniform("uLightOffset", preset.lightOffset) - runtimeShader.setFloatUniform("uSaturateOffset", preset.saturateOffset) - runtimeShader.setFloatUniform("uBound", bound) - isDarkCached = isDark - presetApplied = true - } - - private fun updateBound( - logoHeight: Float, - totalHeight: Float, - totalWidth: Float, - ) { - val heightRatio = logoHeight / totalHeight - if (totalWidth <= totalHeight) { - bound[0] = 0f - bound[1] = 1f - heightRatio - bound[2] = 1f - bound[3] = heightRatio - } else { - val aspectRatio = totalWidth / totalHeight - val contentCenterY = 1f - heightRatio / 2f - bound[0] = 0f - bound[1] = contentCenterY - aspectRatio / 2f - bound[2] = 1f - bound[3] = aspectRatio - } - } - - private data class BgEffectPreset( - val points: FloatArray, - val colors: FloatArray, - val lightOffset: Float, - val saturateOffset: Float, - ) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as BgEffectPreset - - if (lightOffset != other.lightOffset) return false - if (saturateOffset != other.saturateOffset) return false - if (!points.contentEquals(other.points)) return false - if (!colors.contentEquals(other.colors)) return false - - return true - } - - override fun hashCode(): Int { - var result = lightOffset.hashCode() - result = 31 * result + saturateOffset.hashCode() - result = 31 * result + points.contentHashCode() - result = 31 * result + colors.contentHashCode() - return result - } - } - - private companion object { - val PHONE_LIGHT = BgEffectPreset( - points = floatArrayOf( - 0.67f, 0.42f, 1.0f, 0.69f, 0.75f, 1.0f, - 0.14f, 0.71f, 0.95f, 0.14f, 0.27f, 0.8f, - ), - colors = floatArrayOf( - 0.57f, 0.76f, 0.98f, 1.0f, - 0.98f, 0.85f, 0.68f, 1.0f, - 0.98f, 0.75f, 0.93f, 1.0f, - 0.73f, 0.70f, 0.98f, 1.0f, - ), - lightOffset = 0.1f, - saturateOffset = 0.2f, - ) - val PHONE_DARK = BgEffectPreset( - points = floatArrayOf( - 0.63f, 0.50f, 0.88f, 0.69f, 0.75f, 0.80f, - 0.17f, 0.66f, 0.81f, 0.14f, 0.24f, 0.72f, - ), - colors = floatArrayOf( - 0.0f, 0.31f, 0.58f, 1.0f, - 0.53f, 0.29f, 0.15f, 1.0f, - 0.46f, 0.06f, 0.27f, 1.0f, - 0.16f, 0.12f, 0.45f, 1.0f, - ), - lightOffset = -0.1f, - saturateOffset = 0.2f, - ) - } -} - -private const val OS2_BG_FRAG = """ - uniform vec2 uResolution; - uniform shader uTex; - uniform float uAnimTime; - uniform vec4 uBound; - uniform float uTranslateY; - uniform vec3 uPoints[4]; - uniform vec4 uColors[4]; - uniform float uAlphaMulti; - uniform float uNoiseScale; - uniform float uPointOffset; - uniform float uPointRadiusMulti; - uniform float uSaturateOffset; - uniform float uLightOffset; - - vec3 rgb2hsv(vec3 c) - { - vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); - vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); - vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); - float d = q.x - min(q.w, q.y); - float e = 1.0e-10; - return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); - } - - vec3 hsv2rgb(vec3 c) - { - vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); - vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); - return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); - } - - float hash(vec2 p) { - vec3 p3 = fract(vec3(p.xyx) * 0.13); - p3 += dot(p3, p3.yzx + 3.333); - return fract((p3.x + p3.y) * p3.z); - } - - float perlin(vec2 x) { - vec2 i = floor(x); - vec2 f = fract(x); - float a = hash(i); - float b = hash(i + vec2(1.0, 0.0)); - float c = hash(i + vec2(0.0, 1.0)); - float d = hash(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; - } - - float gradientNoise(in vec2 uv) - { - return fract(52.9829189 * fract(dot(uv, vec2(0.06711056, 0.00583715)))); - } - - vec4 main(vec2 fragCoord){ - vec2 vUv = fragCoord/uResolution; - vUv.y = 1.0-vUv.y; - vec2 uv = vUv; - uv -= vec2(0., uTranslateY); - uv.xy -= uBound.xy; - uv.xy /= uBound.zw; - vec3 hsv; - vec4 color = vec4(0.0); - float noiseValue = perlin(vUv * uNoiseScale + vec2(-uAnimTime, -uAnimTime)); - - for (int i = 0; i < 4; i++){ - vec4 pointColor = uColors[i]; - pointColor.rgb *= pointColor.a; - vec2 point = uPoints[i].xy; - float rad = uPoints[i].z * uPointRadiusMulti; - point.x += sin(uAnimTime + point.y) * uPointOffset; - point.y += cos(uAnimTime + point.x) * uPointOffset; - float d = distance(uv, point); - float pct = smoothstep(rad, 0., d); - color.rgb = mix(color.rgb, pointColor.rgb, pct); - color.a = mix(color.a, pointColor.a, pct); - } - - float oppositeNoise = smoothstep(0., 1., noiseValue); - if (color.a > 0.001) { - color.rgb /= color.a; - } else { - color.rgb = vec3(0.0); - } - hsv = rgb2hsv(color.rgb); - hsv.y = mix(hsv.y, 0.0, oppositeNoise * uSaturateOffset); - color.rgb = hsv2rgb(hsv); - color.rgb += oppositeNoise * uLightOffset; - color.a = clamp(color.a, 0., 1.); - color.a *= uAlphaMulti; - - vec4 uiColor = uTex.eval(vec2(vUv.x, 1.0 - vUv.y)*uResolution); - vec4 fragColor; - color.rgb += (10.0 / 255.0) * gradientNoise(fragCoord.xy) - (5.0 / 255.0); - if (uiColor.a < 0.01) { - fragColor = color; - } else { - fragColor = uiColor; - } - return vec4(fragColor.rgb*fragColor.a, fragColor.a); - } -""" diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt index 7eaa6e7..30f6b54 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt @@ -6,6 +6,7 @@ import androidx.activity.compose.LocalActivity import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -40,6 +41,7 @@ import androidx.compose.ui.graphics.BlendModeColorFilter import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle @@ -70,6 +72,9 @@ import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyProfiles +import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar +import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur +import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTileList import io.github.miuzarte.scrcpyforandroid.widgets.PairingCard @@ -102,12 +107,14 @@ import top.yukonga.miuix.kmp.basic.SpinnerEntry import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.basic.Check import top.yukonga.miuix.kmp.icon.extended.Store import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet import top.yukonga.miuix.kmp.overlay.OverlayListPopup -import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles private const val ADB_CONNECT_TIMEOUT_MS = 12_000L private const val ADB_KEEPALIVE_INTERVAL_MS = 3_000L @@ -129,52 +136,63 @@ private data class StartAppRequest( fun DeviceTabScreen( scrollBehavior: ScrollBehavior, scrcpy: Scrcpy, + bottomInnerPadding: Dp, onOpenReorderDevices: () -> Unit, ) { val navigator = LocalRootNavigator.current var showThreePointMenu by rememberSaveable { mutableStateOf(false) } + val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current) + val blurActive = blurBackdrop != null Scaffold( topBar = { - TopAppBar( - title = "设备", - actions = { - IconButton( - onClick = { showThreePointMenu = true }, - holdDownState = showThreePointMenu, - ) { - Icon( - Icons.Rounded.MoreVert, - contentDescription = "更多" + BlurredBar(backdrop = blurBackdrop) { + TopAppBar( + title = "设备", + color = + if (blurActive) Color.Transparent + else colorScheme.surface, + actions = { + IconButton( + onClick = { showThreePointMenu = true }, + holdDownState = showThreePointMenu, + ) { + Icon( + Icons.Rounded.MoreVert, + contentDescription = "更多" + ) + } + DeviceMenuPopup( + show = showThreePointMenu, + onDismissRequest = { showThreePointMenu = false }, + onReorderDevices = { + onOpenReorderDevices() + showThreePointMenu = false + }, + onOpenVirtualButtonOrder = { + navigator.push(RootScreen.VirtualButtonOrder) + showThreePointMenu = false + }, + canClearLogs = EventLogger.hasLogs(), + onClearLogs = { + EventLogger.clearLogs() + showThreePointMenu = false + }, ) - } - DeviceMenuPopup( - show = showThreePointMenu, - onDismissRequest = { showThreePointMenu = false }, - onReorderDevices = { - onOpenReorderDevices() - showThreePointMenu = false - }, - onOpenVirtualButtonOrder = { - navigator.push(RootScreen.VirtualButtonOrder) - showThreePointMenu = false - }, - canClearLogs = EventLogger.hasLogs(), - onClearLogs = { - EventLogger.clearLogs() - showThreePointMenu = false - }, - ) - }, - scrollBehavior = scrollBehavior, - ) + }, + scrollBehavior = scrollBehavior, + ) + } }, ) { pagePadding -> - DeviceTabPage( - contentPadding = pagePadding, - scrollBehavior = scrollBehavior, - scrcpy = scrcpy, - ) + Box(modifier = if (blurActive) Modifier.layerBackdrop(blurBackdrop) else Modifier) { + DeviceTabPage( + contentPadding = pagePadding, + scrollBehavior = scrollBehavior, + scrcpy = scrcpy, + bottomInnerPadding = bottomInnerPadding, + ) + } } } @@ -183,6 +201,7 @@ fun DeviceTabPage( contentPadding: PaddingValues, scrollBehavior: ScrollBehavior, scrcpy: Scrcpy, + bottomInnerPadding: Dp, ) { val activity = LocalActivity.current val fragmentActivity = remember(activity) { activity as? FragmentActivity } @@ -943,6 +962,7 @@ fun DeviceTabPage( contentPadding = contentPadding, scrollBehavior = scrollBehavior, state = listState, + bottomInnerPadding = bottomInnerPadding, ) { item { StatusCard( @@ -1073,8 +1093,8 @@ fun DeviceTabPage( recentTasks.isNotEmpty() -> recentTasks.size.toString() else -> "空" }, - color = MiuixTheme.colorScheme.onSurfaceVariantActions, - fontSize = MiuixTheme.textStyles.body2.fontSize, + color = colorScheme.onSurfaceVariantActions, + fontSize = textStyles.body2.fontSize, modifier = Modifier.padding(end = UiSpacing.ContentVertical), ) }, @@ -1180,7 +1200,6 @@ fun DeviceTabPage( } } - item { Spacer(Modifier.height(UiSpacing.PageBottom)) } } RecentTasksBottomSheet( @@ -1224,7 +1243,6 @@ private fun DeviceMenuPopup( popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, alignment = PopupPositionProvider.Align.TopEnd, onDismissRequest = onDismissRequest, - enableWindowDim = false, ) { ListPopupColumn { DeviceMenuPopupItem( @@ -1376,7 +1394,7 @@ private fun RecentTaskSheetItem( entry.title?.let { Text( text = it, - fontSize = MiuixTheme.textStyles.body1.fontSize, + fontSize = textStyles.body1.fontSize, fontWeight = FontWeight.Medium, color = spinnerColors.contentColor, ) @@ -1384,7 +1402,7 @@ private fun RecentTaskSheetItem( entry.summary?.let { Text( text = it, - fontSize = MiuixTheme.textStyles.body2.fontSize, + fontSize = textStyles.body2.fontSize, color = spinnerColors.summaryColor, ) } @@ -1425,9 +1443,9 @@ private fun DeviceMenuPopupItem( if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem Text( text = text, - fontSize = MiuixTheme.textStyles.body1.fontSize, + fontSize = textStyles.body1.fontSize, fontWeight = FontWeight.Medium, - color = MiuixTheme.colorScheme.disabledOnSecondaryVariant, + color = colorScheme.disabledOnSecondaryVariant, modifier = Modifier .fillMaxWidth() .padding(horizontal = UiSpacing.PopupHorizontal) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt index 5a33036..596f4bb 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt @@ -1,7 +1,6 @@ package io.github.miuzarte.scrcpyforandroid.pages import android.annotation.SuppressLint -import android.app.Activity import android.graphics.Rect import android.util.Log import androidx.activity.compose.BackHandler @@ -38,7 +37,6 @@ import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.sp diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt index 15586c5..b8851b4 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainScreen.kt @@ -10,6 +10,10 @@ import androidx.activity.compose.PredictiveBackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.core.spring +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager @@ -32,13 +36,21 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberDecoratedNavEntries import androidx.navigation3.ui.NavDisplay +import com.kyant.backdrop.backdrops.layerBackdrop +import com.kyant.backdrop.backdrops.rememberLayerBackdrop import io.github.miuzarte.scrcpyforandroid.BuildConfig import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes @@ -55,6 +67,13 @@ import io.github.miuzarte.scrcpyforandroid.storage.AppSettings import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices +import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar +import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur +import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBar +import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBarBlur +import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBar +import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBarItem +import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -62,15 +81,19 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.NavigationBar import top.yukonga.miuix.kmp.basic.NavigationBarItem import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.SnackbarHost import top.yukonga.miuix.kmp.basic.SnackbarHostState +import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.theme.ColorSchemeMode import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.theme.ThemeController +import top.yukonga.miuix.kmp.blur.layerBackdrop as miuixLayerBackdrop private enum class MainBottomTabDestination( val label: String, @@ -316,59 +339,151 @@ fun MainScreen() { val rootEntryProvider = entryProvider { entry(RootScreen.Home) { + val blurBackdrop = rememberBlurBackdrop(enableBlur = asBundle.blur) + val floatingBarBlurActive = asBundle.blur && asBundle.floatingBottomBarBlur + val surfaceColor = colorScheme.surface + val glassBackdrop = rememberLayerBackdrop { + drawRect(surfaceColor) + drawContent() + } + + fun navigateToTab(tab: MainBottomTabDestination) { + scope.launch { + pagerState.animateScrollToPage( + page = tab.ordinal, + animationSpec = spring( + dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO, + stiffness = UiMotion.PAGE_SWITCH_STIFFNESS, + ), + ) + } + } + Scaffold( bottomBar = { - NavigationBar { - tabs.forEach { tab -> - NavigationBarItem( - selected = currentTab == tab, - onClick = { - scope.launch { - pagerState.animateScrollToPage( - page = tab.ordinal, - animationSpec = spring( - dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO, - stiffness = UiMotion.PAGE_SWITCH_STIFFNESS, - ), - ) - } - }, - icon = tab.icon, - label = tab.label, - ) + if (!asBundle.floatingBottomBar) { + BlurredBar(backdrop = blurBackdrop) { + NavigationBar( + color = + if (blurBackdrop != null) Color.Transparent + else colorScheme.surface, + ) { + tabs.forEach { tab -> + NavigationBarItem( + selected = currentTab == tab, + onClick = { navigateToTab(tab) }, + icon = tab.icon, + label = tab.label, + ) + } + } } } }, snackbarHost = { SnackbarHost(snackHostState) }, ) { contentPadding -> - HorizontalPager( - modifier = Modifier - .fillMaxSize() - .padding(bottom = contentPadding.calculateBottomPadding()), - state = pagerState, - beyondViewportPageCount = 1, - ) { page -> - val tab = tabs[page] - saveableStateHolder.SaveableStateProvider(tab.name) { - when (tab) { - MainBottomTabDestination.Device -> DeviceTabScreen( - scrollBehavior = devicesPageScrollBehavior, - scrcpy = scrcpy, - onOpenReorderDevices = { showReorderDevices = true }, - ) - - MainBottomTabDestination.Settings -> SettingsScreen( - scrollBehavior = settingsPageScrollBehavior, - onOpenReorderDevices = { showReorderDevices = true }, - ) - } - } + val bottomInnerPadding: Dp = if (asBundle.floatingBottomBar) { + 12.dp + 64.dp + contentPadding.calculateBottomPadding() + } else { + contentPadding.calculateBottomPadding() } - ReorderDevicesScreen( - show = showReorderDevices, - onDismissRequest = { showReorderDevices = false }, - ) + Box( + modifier = Modifier.fillMaxSize(), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .then(if (blurBackdrop != null) Modifier.miuixLayerBackdrop(blurBackdrop) else Modifier), + ) { + HorizontalPager( + modifier = Modifier + .fillMaxSize() + .then( + if (asBundle.floatingBottomBar && floatingBarBlurActive) { + Modifier.layerBackdrop(glassBackdrop) + } else { + Modifier + } + ), + state = pagerState, + beyondViewportPageCount = 1, + ) { page -> + val tab = tabs[page] + saveableStateHolder.SaveableStateProvider(tab.name) { + when (tab) { + MainBottomTabDestination.Device -> DeviceTabScreen( + scrollBehavior = devicesPageScrollBehavior, + scrcpy = scrcpy, + bottomInnerPadding = bottomInnerPadding, + onOpenReorderDevices = { showReorderDevices = true }, + ) + + MainBottomTabDestination.Settings -> SettingsScreen( + scrollBehavior = settingsPageScrollBehavior, + bottomInnerPadding = bottomInnerPadding, + onOpenReorderDevices = { showReorderDevices = true }, + ) + } + } + } + } + + if (asBundle.floatingBottomBar) { + FloatingBottomBar( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 12.dp + contentPadding.calculateBottomPadding()) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = {}, + ), + selectedIndex = { pagerState.currentPage }, + onSelected = { index -> + scope.launch { + pagerState.animateScrollToPage( + page = index, + animationSpec = spring( + dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO, + stiffness = UiMotion.PAGE_SWITCH_STIFFNESS, + ), + ) + } + }, + backdrop = glassBackdrop, + tabsCount = tabs.size, + isBlurEnabled = floatingBarBlurActive, + ) { + tabs.forEach { tab -> + FloatingBottomBarItem( + onClick = { navigateToTab(tab) }, + modifier = Modifier.defaultMinSize(minWidth = 76.dp), + ) { + Icon( + imageVector = tab.icon, + contentDescription = tab.label, + tint = colorScheme.onSurface, + ) + Text( + text = tab.label, + fontSize = 11.sp, + lineHeight = 14.sp, + color = colorScheme.onSurface, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Visible, + ) + } + } + } + } + + ReorderDevicesScreen( + show = showReorderDevices, + onDismissRequest = { showReorderDevices = false }, + ) + } } } @@ -403,8 +518,14 @@ fun MainScreen() { } val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) } - MiuixTheme(controller = themeController) { + MiuixTheme( + controller = themeController, + smoothRounding = asBundle.smoothCorner, + ) { CompositionLocalProvider( + LocalEnableBlur provides asBundle.blur, + LocalEnableFloatingBottomBar provides asBundle.floatingBottomBar, + LocalEnableFloatingBottomBarBlur provides asBundle.floatingBottomBarBlur, LocalRootNavigator provides rootNavigator, LocalSnackbarController provides snackbarController, LocalAppHaptics provides haptics, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt index e0552ec..06fc36f 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt @@ -3,6 +3,7 @@ package io.github.miuzarte.scrcpyforandroid.pages import android.annotation.SuppressLint import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -34,6 +35,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction @@ -63,6 +65,9 @@ import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyProfiles +import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar +import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur +import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -87,6 +92,7 @@ import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Store import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet @@ -96,7 +102,7 @@ import top.yukonga.miuix.kmp.preference.ArrowPreference import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference import top.yukonga.miuix.kmp.preference.OverlaySpinnerPreference import top.yukonga.miuix.kmp.preference.SwitchPreference -import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import kotlin.math.roundToInt @Composable @@ -105,6 +111,8 @@ internal fun ScrcpyAllOptionsScreen( scrcpy: Scrcpy, ) { val navigator = LocalRootNavigator.current + val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current) + val blurActive = blurBackdrop != null val scope = rememberCoroutineScope() val snackbar = LocalSnackbarController.current var showProfileMenu by rememberSaveable { mutableStateOf(false) } @@ -211,81 +219,92 @@ internal fun ScrcpyAllOptionsScreen( Scaffold( topBar = { - TopAppBar( - title = "所有参数", - navigationIcon = { - IconButton(onClick = navigator.pop) { - Icon( - Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = "返回" + BlurredBar(backdrop = blurBackdrop) { + TopAppBar( + title = "所有参数", + color = + if (blurActive) Color.Transparent + else colorScheme.surface, + navigationIcon = { + IconButton(onClick = navigator.pop) { + Icon( + Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = "返回" + ) + } + }, + actions = { + IconButton( + onClick = { showProfileMenu = true }, + holdDownState = showProfileMenu, + ) { + Icon( + Icons.Rounded.MoreVert, + contentDescription = "配置管理", + ) + } + ProfileMenuPopup( + show = showProfileMenu, + onDismissRequest = { showProfileMenu = false }, + onManageProfiles = { + showManageProfilesSheet = true + showProfileMenu = false + }, ) - } - }, - actions = { - IconButton( - onClick = { showProfileMenu = true }, - holdDownState = showProfileMenu, - ) { - Icon( - Icons.Rounded.MoreVert, - contentDescription = "配置管理", - ) - } - ProfileMenuPopup( - show = showProfileMenu, - onDismissRequest = { showProfileMenu = false }, - onManageProfiles = { - showManageProfilesSheet = true - showProfileMenu = false - }, - ) - }, - scrollBehavior = scrollBehavior, - bottomContent = { - TabRow( - tabs = profileTabs, - selectedTabIndex = selectedProfileIndex, - onTabSelected = { index -> - val nextProfileId = profileIds.getOrNull(index) - ?: return@TabRow - if (nextProfileId == selectedProfileId) return@TabRow - scope.launch { - saveBundleForProfile(selectedProfileId, soBundleState.value) - bindCurrentConnectedDevice(nextProfileId) - selectedProfileId = nextProfileId - val profileName = profileTabs.getOrElse(index) { "全局" } - currentConnectedDeviceName?.let { deviceName -> - snackbar.show("$deviceName 已切换到配置 $profileName") + }, + scrollBehavior = scrollBehavior, + bottomContent = { + TabRow( + tabs = profileTabs, + selectedTabIndex = selectedProfileIndex, + onTabSelected = { index -> + val nextProfileId = profileIds.getOrNull(index) + ?: return@TabRow + if (nextProfileId == selectedProfileId) return@TabRow + scope.launch { + saveBundleForProfile(selectedProfileId, soBundleState.value) + bindCurrentConnectedDevice(nextProfileId) + selectedProfileId = nextProfileId + val profileName = profileTabs.getOrElse(index) { "全局" } + currentConnectedDeviceName?.let { deviceName -> + snackbar.show("$deviceName 已切换到配置 $profileName") + } } - } - }, - modifier = Modifier - .padding(bottom = UiSpacing.Medium) - .padding(horizontal = UiSpacing.Medium), - minWidth = 96.dp, - maxWidth = 192.dp, - height = 48.dp, - itemSpacing = UiSpacing.Medium, - ) - } - ) + }, + modifier = Modifier + .padding(bottom = UiSpacing.Medium) + .padding(horizontal = UiSpacing.Medium), + minWidth = 96.dp, + maxWidth = 192.dp, + height = 48.dp, + itemSpacing = UiSpacing.Medium, + ) + } + ) + } }, snackbarHost = { val snackbar = LocalSnackbarController.current SnackbarHost(snackbar.hostState) }, ) { contentPadding -> - ScrcpyAllOptionsPage( - contentPadding = contentPadding, - scrollBehavior = scrollBehavior, - scrcpy = scrcpy, - soBundleShared = soBundleShared, - scrcpyProfilesState = scrcpyProfilesState, - selectedProfileIdState = selectedProfileIdState, - soBundleState = soBundleState, - lastValidSoBundleState = lastValidSoBundleState, - onSaveBundleForProfile = ::saveBundleForProfile, - ) + Box( + modifier = + if (blurActive) Modifier.layerBackdrop(blurBackdrop) + else Modifier, + ) { + ScrcpyAllOptionsPage( + contentPadding = contentPadding, + scrollBehavior = scrollBehavior, + scrcpy = scrcpy, + soBundleShared = soBundleShared, + scrcpyProfilesState = scrcpyProfilesState, + selectedProfileIdState = selectedProfileIdState, + soBundleState = soBundleState, + lastValidSoBundleState = lastValidSoBundleState, + onSaveBundleForProfile = ::saveBundleForProfile, + ) + } ProfileNameDialog( mode = activeProfileDialog, @@ -752,6 +771,7 @@ internal fun ScrcpyAllOptionsPage( LazyColumn( contentPadding = contentPadding, scrollBehavior = scrollBehavior, + bottomInnerPadding = UiSpacing.PageBottom, ) { item { Card { @@ -1854,7 +1874,6 @@ internal fun ScrcpyAllOptionsPage( } } - item { Spacer(Modifier.height(UiSpacing.PageBottom)) } } } @@ -1874,7 +1893,6 @@ private fun ProfileMenuPopup( popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, alignment = PopupPositionProvider.Align.TopEnd, onDismissRequest = onDismissRequest, - enableWindowDim = false, ) { ListPopupColumn { ProfileMenuPopupItem( @@ -1915,7 +1933,7 @@ private fun ProfileMenuPopupItem( top = if (index == 0) UiSpacing.PopupHorizontal else UiSpacing.PageItem, bottom = if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem, ), - color = MiuixTheme.colorScheme.disabledOnSecondaryVariant, + color = colorScheme.disabledOnSecondaryVariant, fontWeight = FontWeight.Medium, ) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt index dad468f..006c79e 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt @@ -4,12 +4,11 @@ import android.content.Intent import android.os.Process import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Clear @@ -26,8 +25,10 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import io.github.miuzarte.scrcpyforandroid.BuildConfig import io.github.miuzarte.scrcpyforandroid.LockscreenPasswordActivity import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes @@ -42,6 +43,9 @@ import io.github.miuzarte.scrcpyforandroid.storage.AppSettings import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings +import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar +import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur +import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -56,30 +60,48 @@ import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.preference.ArrowPreference import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference import top.yukonga.miuix.kmp.preference.SwitchPreference +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import kotlin.math.roundToInt import kotlin.system.exitProcess @Composable fun SettingsScreen( scrollBehavior: ScrollBehavior, + bottomInnerPadding: Dp, onOpenReorderDevices: () -> Unit, ) { + val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current) + val blurActive = blurBackdrop != null + Scaffold( topBar = { - TopAppBar( - title = "设置", - scrollBehavior = scrollBehavior, - ) + BlurredBar(backdrop = blurBackdrop) { + TopAppBar( + title = "设置", + color = + if (blurActive) Color.Transparent + else colorScheme.surface, + scrollBehavior = scrollBehavior, + ) + } }, ) { pagePadding -> - SettingsPage( - contentPadding = pagePadding, - scrollBehavior = scrollBehavior, - onOpenReorderDevices = onOpenReorderDevices, - ) + Box( + modifier = + if (blurActive) Modifier.layerBackdrop(blurBackdrop) + else Modifier, + ) { + SettingsPage( + contentPadding = pagePadding, + scrollBehavior = scrollBehavior, + bottomInnerPadding = bottomInnerPadding, + onOpenReorderDevices = onOpenReorderDevices, + ) + } } } @@ -87,6 +109,7 @@ fun SettingsScreen( fun SettingsPage( contentPadding: PaddingValues, scrollBehavior: ScrollBehavior, + bottomInnerPadding: Dp, onOpenReorderDevices: () -> Unit, ) { val context = LocalContext.current @@ -172,6 +195,7 @@ fun SettingsPage( LazyColumn( contentPadding = contentPadding, scrollBehavior = scrollBehavior, + bottomInnerPadding = bottomInnerPadding, ) { item { SectionSmallTitle("主题") @@ -180,30 +204,60 @@ fun SettingsPage( title = "外观模式", summary = ThemeModes.baseOptions .getOrNull( - asBundle.themeBaseIndex.coerceIn( - 0, - ThemeModes.baseOptions.lastIndex - ) + asBundle.themeBaseIndex + .coerceIn(0, ThemeModes.baseOptions.lastIndex) ) ?.label ?: "跟随系统", items = themeItems, - selectedIndex = asBundle.themeBaseIndex.coerceIn( - 0, - ThemeModes.baseOptions.lastIndex - ), + selectedIndex = asBundle.themeBaseIndex + .coerceIn(0, ThemeModes.baseOptions.lastIndex), onSelectedIndexChange = { asBundle = asBundle.copy(themeBaseIndex = it) }, ) SwitchPreference( - title = "Monet", + title = "Monet 颜色", summary = "开启后使用 Monet 动态配色", checked = asBundle.monet, onCheckedChange = { asBundle = asBundle.copy(monet = it) }, ) + SwitchPreference( + title = "模糊", + summary = "启用顶栏和底栏的模糊效果", + checked = asBundle.blur, + onCheckedChange = { + asBundle = asBundle.copy(blur = it) + } + ) + SwitchPreference( + title = "悬浮底栏", + summary = "使用 Apple 风格的悬浮底栏", + checked = asBundle.floatingBottomBar, + onCheckedChange = { + asBundle = asBundle.copy(floatingBottomBar = it) + } + ) + AnimatedVisibility(asBundle.floatingBottomBar) { + SwitchPreference( + title = "液态玻璃", + summary = "启用悬浮底栏的液态玻璃效果", + checked = asBundle.floatingBottomBarBlur, + onCheckedChange = { + asBundle = asBundle.copy(floatingBottomBarBlur = it) + } + ) + } + SwitchPreference( + title = "平滑圆角", + summary = "启用全局平滑圆角效果", + checked = asBundle.smoothCorner, + onCheckedChange = { + asBundle = asBundle.copy(smoothCorner = it) + } + ) } } @@ -500,6 +554,5 @@ fun SettingsPage( } } - item { Spacer(Modifier.height(UiSpacing.PageBottom)) } } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/StreamScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/StreamScreen.kt index d08cbf3..04456bc 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/StreamScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/StreamScreen.kt @@ -99,7 +99,10 @@ fun StreamScreen(activity: StreamActivity) { SnackbarController(scope = snackbarScope, hostState = snackbarHostState) } - MiuixTheme(controller = themeController) { + MiuixTheme( + controller = themeController, + smoothRounding = asBundle.smoothCorner, + ) { CompositionLocalProvider( LocalSnackbarController provides snackbarController, LocalAppHaptics provides haptics, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt index ecf05ba..d9d8942 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderScreen.kt @@ -1,5 +1,6 @@ package io.github.miuzarte.scrcpyforandroid.pages +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -12,16 +13,19 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings +import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar +import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur +import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions @@ -36,34 +40,45 @@ import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.preference.SwitchPreference +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme @Composable internal fun VirtualButtonOrderScreen( scrollBehavior: ScrollBehavior, ) { val navigator = LocalRootNavigator.current + val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current) + val blurActive = blurBackdrop != null Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - TopAppBar( - title = "虚拟按钮排序", - navigationIcon = { - IconButton(onClick = navigator.pop) { - Icon( - Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = "返回" - ) - } - }, - scrollBehavior = scrollBehavior, - ) + BlurredBar(backdrop = blurBackdrop) { + TopAppBar( + title = "虚拟按钮排序", + color = + if (blurActive) Color.Transparent + else colorScheme.surface, + navigationIcon = { + IconButton(onClick = navigator.pop) { + Icon( + Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = "返回" + ) + } + }, + scrollBehavior = scrollBehavior, + ) + } }, ) { pagePadding -> - VirtualButtonOrderPage( - contentPadding = pagePadding, - scrollBehavior = scrollBehavior, - ) + Box(modifier = if (blurActive) Modifier.layerBackdrop(blurBackdrop) else Modifier) { + VirtualButtonOrderPage( + contentPadding = pagePadding, + scrollBehavior = scrollBehavior, + ) + } } } @@ -72,7 +87,6 @@ internal fun VirtualButtonOrderPage( contentPadding: PaddingValues, scrollBehavior: ScrollBehavior, ) { - val scope = rememberCoroutineScope() val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val asBundleShared by appSettings.bundleState.collectAsState() @@ -105,8 +119,8 @@ internal fun VirtualButtonOrderPage( LazyColumn( contentPadding = contentPadding, scrollBehavior = scrollBehavior, + bottomInnerPadding = UiSpacing.PageBottom, ) { - // 按钮显示文本开关 item { Card { SwitchPreference( @@ -130,8 +144,12 @@ internal fun VirtualButtonOrderPage( ReorderableList.Item( id = action.id, icon = action.icon, - title = if (action.keycode == null) action.title else "${action.title} (${action.keycode})", - subtitle = if (item.showOutside) "显示在外部" else "显示在更多菜单内", + title = + if (action.keycode == null) action.title + else "${action.title} (${action.keycode})", + subtitle = + if (item.showOutside) "显示在外部" + else "显示在更多菜单内", endActions = listOf( ReorderableList.EndAction.Checkbox( checked = item.showOutside, @@ -139,16 +157,13 @@ internal fun VirtualButtonOrderPage( onClick = { val checked = !item.showOutside buttonItems = buttonItems.map { current -> - if (current.action.id == action.id) { + if (current.action.id == action.id) current.copy(showOutside = checked) - } else { - current - } + else current } asBundle = asBundle.copy( - virtualButtonsLayout = VirtualButtonActions.encodeStoredLayout( - buttonItems - ) + virtualButtonsLayout = VirtualButtonActions + .encodeStoredLayout(buttonItems) ) }, ) @@ -158,11 +173,11 @@ internal fun VirtualButtonOrderPage( }, orientation = ReorderableList.Orientation.Column, onSettle = { fromIndex, toIndex -> - buttonItems = buttonItems.toMutableList().apply { - add(toIndex, removeAt(fromIndex)) - } + buttonItems = buttonItems.toMutableList() + .apply { add(toIndex, removeAt(fromIndex)) } asBundle = asBundle.copy( - virtualButtonsLayout = VirtualButtonActions.encodeStoredLayout(buttonItems) + virtualButtonsLayout = VirtualButtonActions + .encodeStoredLayout(buttonItems) ) }, )() diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectBackground.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectBackground.kt new file mode 100644 index 0000000..ee3c31d --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectBackground.kt @@ -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() + } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfig.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfig.kt new file mode 100644 index 0000000..c0f7ad5 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfig.kt @@ -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) + } + } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfigOs2.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfigOs2.kt new file mode 100644 index 0000000..b5a43ab --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfigOs2.kt @@ -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 + } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfigOs3.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfigOs3.kt new file mode 100644 index 0000000..f8042e5 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectConfigOs3.kt @@ -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 + } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectPainter.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectPainter.kt new file mode 100644 index 0000000..2ecf018 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/BgEffectPainter.kt @@ -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 + } + } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/FrameTime.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/FrameTime.kt new file mode 100644 index 0000000..6c528e9 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/FrameTime.kt @@ -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 } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/OS2BgFrag.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/OS2BgFrag.kt new file mode 100644 index 0000000..5e6228a --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/OS2BgFrag.kt @@ -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); + } +""" diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/OS3BgFrag.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/OS3BgFrag.kt new file mode 100644 index 0000000..e9ff1e4 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/effect/OS3BgFrag.kt @@ -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); + } +""" diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/BiometricGate.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/BiometricGate.kt index 1391027..f6b9831 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/BiometricGate.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/BiometricGate.kt @@ -2,16 +2,12 @@ package io.github.miuzarte.scrcpyforandroid.password import android.app.KeyguardManager import android.content.Context -import android.os.Build import android.os.Looper -import androidx.annotation.RequiresApi import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity -import io.github.miuzarte.scrcpyforandroid.nativecore.AdbMdnsDiscoverer import io.github.miuzarte.scrcpyforandroid.services.AppRuntime -import io.github.miuzarte.scrcpyforandroid.storage.Storage import kotlinx.coroutines.suspendCancellableCoroutine import java.util.concurrent.atomic.AtomicLong import kotlin.coroutines.resume @@ -30,7 +26,8 @@ object BiometricGate { } fun isDeviceSecure(): Boolean { - val manager = AppRuntime.context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + val manager = AppRuntime.context + .getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager return manager.isDeviceSecure } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/PasswordPickerPopup.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/PasswordPickerPopup.kt index 8ea05e6..b9ffe70 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/PasswordPickerPopup.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/PasswordPickerPopup.kt @@ -23,7 +23,7 @@ import top.yukonga.miuix.kmp.basic.SpinnerDefaults import top.yukonga.miuix.kmp.basic.SpinnerEntry import top.yukonga.miuix.kmp.basic.SpinnerItemImpl import top.yukonga.miuix.kmp.basic.Text -import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme @Composable fun PasswordPickerPopupContent( @@ -67,7 +67,7 @@ fun PasswordPickerPopupContent( modifier = Modifier .fillMaxWidth() .padding(UiSpacing.PopupHorizontal), - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + color = colorScheme.onSurfaceVariantSummary, fontWeight = FontWeight.Medium, ) return@ListPopupColumn @@ -79,7 +79,7 @@ fun PasswordPickerPopupContent( modifier = Modifier .fillMaxWidth() .padding(UiSpacing.PopupHorizontal), - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + color = colorScheme.onSurfaceVariantSummary, fontWeight = FontWeight.Medium, ) return@ListPopupColumn diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/PasswordRepository.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/PasswordRepository.kt index 3ab8389..30a7bd4 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/PasswordRepository.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/password/PasswordRepository.kt @@ -6,7 +6,6 @@ import androidx.core.content.edit import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import io.github.miuzarte.scrcpyforandroid.services.AppRuntime -import io.github.miuzarte.scrcpyforandroid.storage.Storage import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/LazyColumn.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/LazyColumn.kt index 5a9f436..4429f46 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/LazyColumn.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/LazyColumn.kt @@ -3,8 +3,9 @@ package io.github.miuzarte.scrcpyforandroid.scaffolds import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState @@ -14,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing @@ -29,11 +31,20 @@ fun LazyColumn( itemSpacing: Dp = UiSpacing.PageItem, horizontalPadding: Dp = UiSpacing.PageHorizontal, verticalPadding: Dp = UiSpacing.PageVertical, + bottomInnerPadding: Dp? = null, clearFocusOnTap: Boolean = true, content: LazyListScope.() -> Unit, ) { + val layoutDirection = LocalLayoutDirection.current val focusManager = LocalFocusManager.current + val mergedContentPadding = PaddingValues( + start = contentPadding.calculateLeftPadding(layoutDirection) + horizontalPadding, + top = contentPadding.calculateTopPadding() + verticalPadding, + end = contentPadding.calculateRightPadding(layoutDirection) + horizontalPadding, + bottom = contentPadding.calculateBottomPadding() + verticalPadding, + ) + LazyColumn( modifier = modifier .fillMaxSize() @@ -48,11 +59,16 @@ fun LazyColumn( if (scrollBehavior != null) Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) else Modifier - ) - .padding(contentPadding), + ), state = state, - contentPadding = PaddingValues(horizontal = horizontalPadding, vertical = verticalPadding), + contentPadding = mergedContentPadding, verticalArrangement = Arrangement.spacedBy(itemSpacing), - content = content, - ) + ) { + content() + bottomInnerPadding?.let { padding -> + (padding - itemSpacing) + .takeIf { it > 0.dp } + ?.let { item { Spacer(Modifier.height(it)) } } + } + } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperSlider.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperSlider.kt index 9cb6f83..472f0ba 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperSlider.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperSlider.kt @@ -22,7 +22,8 @@ import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.overlay.OverlayDialog import top.yukonga.miuix.kmp.preference.ArrowPreference -import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles @Composable fun SuperSlider( @@ -69,8 +70,8 @@ fun SuperSlider( val text = if (shouldShowUnit) "$valueText $unit" else valueText Text( text = text, - fontSize = MiuixTheme.textStyles.body2.fontSize, - color = MiuixTheme.colorScheme.onSurfaceVariantActions, + fontSize = textStyles.body2.fontSize, + color = colorScheme.onSurfaceVariantActions, ) }, enabled = enabled, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperTextField.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperTextField.kt index c00f273..88bdae9 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperTextField.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/SuperTextField.kt @@ -22,7 +22,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import top.yukonga.miuix.kmp.basic.TextField -import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles /** * A wrapped [TextField] component with state change callbacks. @@ -44,7 +45,6 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme * @param onFocusLost The callback to be called when the text field loses focus. * @param insideMargin The margin inside the [TextField]. * @param backgroundColor The background color of the [TextField]. - * @param cornerRadius The corner radius of the [TextField]. * @param labelColor The color of the label. * @param borderColor The color of the border when the [TextField] is focused. * @param textStyle The text style to be applied to the [TextField]. @@ -70,12 +70,11 @@ fun SuperTextField( onFocusGained: (() -> Unit)? = null, onFocusLost: (() -> Unit)? = null, insideMargin: DpSize = DpSize(16.dp, 16.dp), - backgroundColor: Color = MiuixTheme.colorScheme.secondaryContainer, - cornerRadius: Dp = 16.dp, - labelColor: Color = MiuixTheme.colorScheme.onSecondaryContainer, - borderColor: Color = MiuixTheme.colorScheme.primary, - textStyle: TextStyle = MiuixTheme.textStyles.main, - cursorBrush: Brush = SolidColor(MiuixTheme.colorScheme.primary), + backgroundColor: Color = colorScheme.secondaryContainer, + labelColor: Color = colorScheme.onSecondaryContainer, + borderColor: Color = colorScheme.primary, + textStyle: TextStyle = textStyles.main, + cursorBrush: Brush = SolidColor(colorScheme.primary), leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, ) { @@ -109,7 +108,6 @@ fun SuperTextField( visualTransformation = visualTransformation, insideMargin = insideMargin, backgroundColor = backgroundColor, - cornerRadius = cornerRadius, labelColor = labelColor, borderColor = borderColor, textStyle = textStyle, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppRuntime.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppRuntime.kt index 5cf5e96..a55bd79 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppRuntime.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppRuntime.kt @@ -1,8 +1,6 @@ package io.github.miuzarte.scrcpyforandroid.services import android.content.Context -import android.os.Build -import androidx.annotation.RequiresApi import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget import io.github.miuzarte.scrcpyforandroid.nativecore.AdbMdnsDiscoverer import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy @@ -11,7 +9,6 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy object AppRuntime { private lateinit var appContext: Context - @RequiresApi(Build.VERSION_CODES.R) fun init(context: Context) { appContext = context.applicationContext AdbMdnsDiscoverer.init(appContext) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/PictureInPictureActionReceiver.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/PictureInPictureActionReceiver.kt index dc95157..ff38b73 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/PictureInPictureActionReceiver.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/PictureInPictureActionReceiver.kt @@ -5,15 +5,10 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.os.Build -import androidx.annotation.RequiresApi -import io.github.miuzarte.scrcpyforandroid.password.BiometricGate -import io.github.miuzarte.scrcpyforandroid.storage.Storage import kotlinx.coroutines.runBlocking // not working in MIUI class PictureInPictureActionReceiver : BroadcastReceiver() { - @RequiresApi(Build.VERSION_CODES.R) override fun onReceive(context: Context, intent: Intent?) { if (intent?.action != ACTION_STOP_SCRCPY) return val pendingResult = goAsync() diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt index 6bc465a..8f2ef21 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/AppSettings.kt @@ -22,6 +22,22 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") { booleanPreferencesKey("monet"), false ) + val BLUR = Pair( + booleanPreferencesKey("blur"), + true, + ) + val FLOATING_BOTTOM_BAR = Pair( + booleanPreferencesKey("floating_bottom_bar"), + false, + ) + val FLOATING_BOTTOM_BAR_BLUR = Pair( + booleanPreferencesKey("floating_bottom_bar_blur"), + false, + ) + val SMOOTH_CORNER = Pair( + booleanPreferencesKey("smooth_corner"), + false, + ) val FULLSCREEN_DEBUG_INFO = Pair( booleanPreferencesKey("fullscreen_debug_info"), false @@ -100,6 +116,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") { // Theme Settings val themeBaseIndex by setting(THEME_BASE_INDEX) val monet by setting(MONET) + val blur by setting(BLUR) + val floatingBottomBar by setting(FLOATING_BOTTOM_BAR) + val floatingBottomBarBlur by setting(FLOATING_BOTTOM_BAR_BLUR) + val smoothCorner by setting(SMOOTH_CORNER) // Scrcpy Settings val fullscreenDebugInfo by setting(FULLSCREEN_DEBUG_INFO) @@ -129,6 +149,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") { data class Bundle( val themeBaseIndex: Int, val monet: Boolean, + val blur: Boolean, + val floatingBottomBar: Boolean, + val floatingBottomBarBlur: Boolean, + val smoothCorner: Boolean, val fullscreenDebugInfo: Boolean, val showFullscreenVirtualButtons: Boolean, val showFullscreenFloatingButton: Boolean, @@ -153,6 +177,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") { private val bundleFields = arrayOf( bundleField(THEME_BASE_INDEX) { bundle: Bundle -> bundle.themeBaseIndex }, bundleField(MONET) { bundle: Bundle -> bundle.monet }, + bundleField(BLUR) { bundle: Bundle -> bundle.blur }, + bundleField(FLOATING_BOTTOM_BAR) { bundle: Bundle -> bundle.floatingBottomBar }, + bundleField(FLOATING_BOTTOM_BAR_BLUR) { bundle: Bundle -> bundle.floatingBottomBarBlur }, + bundleField(SMOOTH_CORNER) { bundle: Bundle -> bundle.smoothCorner }, bundleField(FULLSCREEN_DEBUG_INFO) { bundle: Bundle -> bundle.fullscreenDebugInfo }, bundleField(SHOW_FULLSCREEN_VIRTUAL_BUTTONS) { bundle: Bundle -> bundle.showFullscreenVirtualButtons }, bundleField(SHOW_FULLSCREEN_FLOATING_BUTTON) { bundle: Bundle -> bundle.showFullscreenFloatingButton }, @@ -178,6 +206,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") { private fun bundleFromPreferences(preferences: Preferences) = Bundle( themeBaseIndex = preferences.read(THEME_BASE_INDEX), monet = preferences.read(MONET), + blur = preferences.read(BLUR), + floatingBottomBar = preferences.read(FLOATING_BOTTOM_BAR), + floatingBottomBarBlur = preferences.read(FLOATING_BOTTOM_BAR_BLUR), + smoothCorner = preferences.read(SMOOTH_CORNER), fullscreenDebugInfo = preferences.read(FULLSCREEN_DEBUG_INFO), showFullscreenVirtualButtons = preferences.read(SHOW_FULLSCREEN_VIRTUAL_BUTTONS), showFullscreenFloatingButton = preferences.read(SHOW_FULLSCREEN_FLOATING_BUTTON), diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/ThemePreferences.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/ThemePreferences.kt new file mode 100644 index 0000000..23409de --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/ThemePreferences.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/FloatingBottomBar.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/FloatingBottomBar.kt new file mode 100644 index 0000000..2a31724 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/FloatingBottomBar.kt @@ -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() }) + ) + } + } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/animation/DampedDragAnimation.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/animation/DampedDragAnimation.kt new file mode 100644 index 0000000..ca33f4f --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/animation/DampedDragAnimation.kt @@ -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, + 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) } + } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/animation/InteractiveHighlight.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/animation/InteractiveHighlight.kt new file mode 100644 index 0000000..d904fb3 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/animation/InteractiveHighlight.kt @@ -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) } + } + } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/modifier/DragGestureInspector.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/modifier/DragGestureInspector.kt new file mode 100644 index 0000000..166a1b7 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/ui/component/miuix/modifier/DragGestureInspector.kt @@ -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 + } + } + } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt index 31ba23d..1c958e8 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt @@ -69,6 +69,7 @@ import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade import io.github.miuzarte.scrcpyforandroid.constants.Defaults import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing +import io.github.miuzarte.scrcpyforandroid.haptics.LocalAppHaptics import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider @@ -101,7 +102,7 @@ import top.yukonga.miuix.kmp.preference.CheckboxLocation import top.yukonga.miuix.kmp.preference.CheckboxPreference import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference import top.yukonga.miuix.kmp.preference.SwitchPreference -import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.theme.MiuixTheme.isDynamicColor import top.yukonga.miuix.kmp.utils.PressFeedbackType import kotlin.math.roundToInt @@ -132,17 +133,17 @@ internal fun StatusCard( val spec = when { streaming && sessionInfo != null -> { val streamCardColor = when { - isDynamicColor -> MiuixTheme.colorScheme.secondaryContainer + isDynamicColor -> colorScheme.secondaryContainer isDarkTheme -> Color(0xFF1A3825) else -> Color(0xFFDFFAE4) } val streamTextColor = when { - isDynamicColor -> MiuixTheme.colorScheme.onSecondaryContainer + isDynamicColor -> colorScheme.onSecondaryContainer isDarkTheme -> Color.White else -> Color(0xFF111111) } val streamIconColor = if (isDynamicColor) { - MiuixTheme.colorScheme.primary.copy(alpha = 0.8f) + colorScheme.primary.copy(alpha = 0.8f) } else { Color(0xFF36D167) } @@ -171,11 +172,11 @@ internal fun StatusCard( big = StatusBigCardSpec( title = "ADB 已连接", subtitle = cleanStatusLine, - containerColor = MiuixTheme.colorScheme.primaryContainer, - titleColor = MiuixTheme.colorScheme.onPrimaryContainer, - subtitleColor = MiuixTheme.colorScheme.onPrimaryContainer, + containerColor = colorScheme.primaryContainer, + titleColor = colorScheme.onPrimaryContainer, + subtitleColor = colorScheme.onPrimaryContainer, icon = Icons.Rounded.Wifi, - iconTint = MiuixTheme.colorScheme.primary.copy(alpha = 0.6f), + iconTint = colorScheme.primary.copy(alpha = 0.6f), ), firstSmall = StatusSmallCardSpec( "当前设备", @@ -191,11 +192,11 @@ internal fun StatusCard( big = StatusBigCardSpec( title = "ADB 未连接", subtitle = "", - containerColor = MiuixTheme.colorScheme.secondaryContainer, - titleColor = MiuixTheme.colorScheme.onSecondaryContainer, - subtitleColor = MiuixTheme.colorScheme.onSecondaryContainer, + containerColor = colorScheme.secondaryContainer, + titleColor = colorScheme.onSecondaryContainer, + subtitleColor = colorScheme.onSecondaryContainer, icon = Icons.Rounded.LinkOff, - iconTint = MiuixTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.6f), + iconTint = colorScheme.onSecondaryContainer.copy(alpha = 0.6f), ), firstSmall = StatusSmallCardSpec( "当前设备", @@ -983,8 +984,8 @@ internal fun DeviceTile( Card( colors = CardDefaults.defaultColors( color = - if (isConnected) MiuixTheme.colorScheme.surfaceContainer - else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f), + if (isConnected) colorScheme.surfaceContainer + else colorScheme.surfaceContainer.copy(alpha = 0.6f), ), pressFeedbackType = if (!editing) PressFeedbackType.Sink else PressFeedbackType.None, onClick = haptics.contextClick, @@ -1015,7 +1016,7 @@ internal fun DeviceTile( .background( color = if (isConnected) Color(0xFF44C74F) - else MiuixTheme.colorScheme.outline, + else colorScheme.outline, shape = CircleShape, ), ) @@ -1027,12 +1028,12 @@ internal fun DeviceTile( fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = MiuixTheme.colorScheme.onSurface, + color = colorScheme.onSurface, ) Text( "${device.host}:${device.port}", fontSize = 13.sp, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + color = colorScheme.onSurfaceVariantSummary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -1206,14 +1207,16 @@ internal fun QuickConnectCard( onAddDevice: () -> Unit, enabled: Boolean = true, ) { + val haptics = LocalAppHaptics.current val focusManager = LocalFocusManager.current Card( - colors = CardDefaults.defaultColors(color = MiuixTheme.colorScheme.primaryContainer), + colors = CardDefaults.defaultColors(color = colorScheme.primaryContainer), pressFeedbackType = if (enabled) PressFeedbackType.Tilt else PressFeedbackType.None, insideMargin = PaddingValues(UiSpacing.Content), + onClick = haptics.contextClick, ) { Column(verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical)) { Row( @@ -1223,12 +1226,12 @@ internal fun QuickConnectCard( Icon( Icons.Rounded.AddLink, contentDescription = "快速连接", - tint = MiuixTheme.colorScheme.onPrimaryContainer, + tint = colorScheme.onPrimaryContainer, ) Text( "快速连接", fontWeight = FontWeight.Bold, - color = MiuixTheme.colorScheme.onPrimaryContainer, + color = colorScheme.onPrimaryContainer, ) } SuperTextField( diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/ReorderableList.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/ReorderableList.kt index 1b58bc1..66c5aa4 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/ReorderableList.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/ReorderableList.kt @@ -30,7 +30,7 @@ import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Checkbox import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.Text -import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme class ReorderableList( private val itemsProvider: () -> List, @@ -114,12 +114,12 @@ class ReorderableList( Column { Text( text = item.title, - color = MiuixTheme.colorScheme.onSurface, + color = colorScheme.onSurface, fontWeight = FontWeight.SemiBold, ) if (item.subtitle.isNotBlank()) Text( text = item.subtitle, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + color = colorScheme.onSurfaceVariantSummary, fontSize = 13.sp, ) } @@ -152,7 +152,7 @@ class ReorderableList( Icon( Icons.Rounded.DragIndicator, contentDescription = "拖动排序", - tint = MiuixTheme.colorScheme.onSurfaceVariantSummary, + tint = colorScheme.onSurfaceVariantSummary, ) } } @@ -218,7 +218,7 @@ class ReorderableList( Icon( Icons.Rounded.DragIndicator, contentDescription = "拖动排序", - tint = MiuixTheme.colorScheme.onSurfaceVariantSummary, + tint = colorScheme.onSurfaceVariantSummary, ) } } @@ -226,13 +226,13 @@ class ReorderableList( Spacer(Modifier.padding(UiSpacing.ContentVertical)) Text( text = item.title, - color = MiuixTheme.colorScheme.onSurface, + color = colorScheme.onSurface, fontWeight = FontWeight.SemiBold, ) if (item.subtitle.isNotBlank()) { Text( text = item.subtitle, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + color = colorScheme.onSurfaceVariantSummary, fontSize = 13.sp, ) } @@ -266,7 +266,7 @@ private fun EndActionView( contentDescription = action.contentDescription.ifBlank { fallbackContentDescription }, - tint = MiuixTheme.colorScheme.onSurfaceVariantSummary, + tint = colorScheme.onSurfaceVariantSummary, ) } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt index 0833994..c9be2e5 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt @@ -30,7 +30,7 @@ import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.CardDefaults.defaultColors import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.Text -import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.PressFeedbackType @Immutable @@ -122,7 +122,7 @@ internal fun StatusCardLayout( Text( text = busyLabel, fontSize = 12.sp, - color = MiuixTheme.colorScheme.primary, + color = colorScheme.primary, ) } } @@ -164,13 +164,13 @@ private fun StatusMetricCard(spec: StatusSmallCardSpec, modifier: Modifier) { text = spec.title, fontSize = 15.sp, fontWeight = FontWeight.Medium, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + color = colorScheme.onSurfaceVariantSummary, ) Text( text = spec.value, fontSize = 24.sp, fontWeight = FontWeight.SemiBold, - color = MiuixTheme.colorScheme.onSurface, + color = colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt index a80f3cf..f2c629a 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt @@ -66,7 +66,7 @@ import top.yukonga.miuix.kmp.basic.SpinnerEntry import top.yukonga.miuix.kmp.basic.SpinnerItemImpl import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.overlay.OverlayListPopup -import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme enum class VirtualButtonAction( val id: String, @@ -216,10 +216,10 @@ class VirtualButtonBar( passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null, ) { val haptics = LocalAppHaptics.current - val activeContainerColor = MiuixTheme.colorScheme.primary - val disabledContainerColor = MiuixTheme.colorScheme.primary.copy(alpha = 0.35f) - val activeContentColor = MiuixTheme.colorScheme.onPrimary - val disabledContentColor = MiuixTheme.colorScheme.onPrimary.copy(alpha = 0.45f) + val activeContainerColor = colorScheme.primary + val disabledContainerColor = colorScheme.primary.copy(alpha = 0.35f) + val activeContentColor = colorScheme.onPrimary + val disabledContentColor = colorScheme.onPrimary.copy(alpha = 0.45f) var showMorePopup by remember { mutableStateOf(false) } Row( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8683c13..3356b72 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ junit = "4.13.2" androidxJunit = "1.3.0" espressoCore = "3.7.0" miuix = "0.9.0" +backdrop = "1.0.6" material = "1.13.0" runtime = "1.10.5" androidxCorePip = "1.0.0-alpha02" @@ -38,6 +39,7 @@ miuix-blur = { group = "top.yukonga.miuix.kmp", name = "miuix-blur", version.ref miuix-preference = { group = "top.yukonga.miuix.kmp", name = "miuix-preference", version.ref = "miuix" } miuix-icons = { group = "top.yukonga.miuix.kmp", name = "miuix-icons", version.ref = "miuix" } miuix-navigation3-ui = { group = "top.yukonga.miuix.kmp", name = "miuix-navigation3-ui", version.ref = "miuix" } +backdrop = { group = "io.github.kyant0", name = "backdrop", version.ref = "backdrop" } androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" } androidx-core-pip = { group = "androidx.core", name = "core-pip", version.ref = "androidxCorePip" } androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidxBiometric" }