refactor: UI refresh
- 仍然是抄的 KernelSU 管理器和 miuix example - TopAppBar 与 dock 栏模糊 - 液态玻璃(`com.kyant.backdrop`)悬浮 dock 栏
This commit is contained in:
@@ -40,8 +40,8 @@ android {
|
|||||||
applicationId = "io.github.miuzarte.scrcpyforandroid"
|
applicationId = "io.github.miuzarte.scrcpyforandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 11
|
versionCode = 12
|
||||||
versionName = "0.1.5"
|
versionName = "0.1.6"
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
@@ -114,6 +114,7 @@ dependencies {
|
|||||||
implementation(libs.miuix.preference)
|
implementation(libs.miuix.preference)
|
||||||
implementation(libs.miuix.icons)
|
implementation(libs.miuix.icons)
|
||||||
implementation(libs.miuix.navigation3.ui)
|
implementation(libs.miuix.navigation3.ui)
|
||||||
|
implementation(libs.backdrop)
|
||||||
implementation("io.github.vvb2060.ndk:boringssl:20250114")
|
implementation("io.github.vvb2060.ndk:boringssl:20250114")
|
||||||
implementation("org.lsposed.libcxx:libcxx:27.0.12077973")
|
implementation("org.lsposed.libcxx:libcxx:27.0.12077973")
|
||||||
implementation("org.bouncycastle:bcpkix-jdk18on:1.80")
|
implementation("org.bouncycastle:bcpkix-jdk18on:1.80")
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import androidx.compose.runtime.rememberUpdatedState
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
@@ -55,6 +56,7 @@ import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
|
|||||||
import io.github.miuzarte.scrcpyforandroid.services.SnackbarController
|
import io.github.miuzarte.scrcpyforandroid.services.SnackbarController
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList
|
import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -80,6 +82,7 @@ import top.yukonga.miuix.kmp.basic.Text
|
|||||||
import top.yukonga.miuix.kmp.basic.TextButton
|
import top.yukonga.miuix.kmp.basic.TextButton
|
||||||
import top.yukonga.miuix.kmp.basic.TextField
|
import top.yukonga.miuix.kmp.basic.TextField
|
||||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||||
|
import top.yukonga.miuix.kmp.blur.layerBackdrop
|
||||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||||
import top.yukonga.miuix.kmp.icon.extended.Close
|
import top.yukonga.miuix.kmp.icon.extended.Close
|
||||||
import top.yukonga.miuix.kmp.icon.extended.Ok
|
import top.yukonga.miuix.kmp.icon.extended.Ok
|
||||||
@@ -90,6 +93,8 @@ import top.yukonga.miuix.kmp.preference.ArrowPreference
|
|||||||
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
||||||
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
|
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
|
||||||
import top.yukonga.miuix.kmp.theme.ThemeController
|
import top.yukonga.miuix.kmp.theme.ThemeController
|
||||||
|
|
||||||
class LockscreenPasswordActivity : FragmentActivity() {
|
class LockscreenPasswordActivity : FragmentActivity() {
|
||||||
@@ -114,7 +119,10 @@ class LockscreenPasswordActivity : FragmentActivity() {
|
|||||||
remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
|
remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
|
||||||
val haptics = rememberAppHaptics()
|
val haptics = rememberAppHaptics()
|
||||||
|
|
||||||
MiuixTheme(controller = themeController) {
|
MiuixTheme(
|
||||||
|
controller = themeController,
|
||||||
|
smoothRounding = asBundle.smoothCorner,
|
||||||
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalSnackbarController provides snackbarController,
|
LocalSnackbarController provides snackbarController,
|
||||||
LocalAppHaptics provides haptics,
|
LocalAppHaptics provides haptics,
|
||||||
@@ -213,10 +221,18 @@ private fun LockscreenPasswordScreen(
|
|||||||
pendingCreate = false
|
pendingCreate = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val blurBackdrop = rememberBlurBackdrop(asBundle.blur)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = "锁屏密码自动填充",
|
title = "锁屏密码自动填充",
|
||||||
|
modifier =
|
||||||
|
if (blurBackdrop != null) Modifier.layerBackdrop(blurBackdrop)
|
||||||
|
else Modifier,
|
||||||
|
color =
|
||||||
|
if (blurBackdrop != null) Color.Transparent
|
||||||
|
else colorScheme.surface,
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { activity.onBackPressedDispatcher.onBackPressed() }) {
|
IconButton(onClick = { activity.onBackPressedDispatcher.onBackPressed() }) {
|
||||||
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "返回")
|
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "返回")
|
||||||
@@ -442,6 +458,7 @@ private fun LockscreenPasswordPage(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
bottomInnerPadding = UiSpacing.PageBottom,
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Card {
|
Card {
|
||||||
@@ -514,16 +531,14 @@ private fun LockscreenPasswordPage(
|
|||||||
"\n2. 在 root / posed / hook / 调试器 / 恶意输入法 等环境下,密码仍可能泄露" +
|
"\n2. 在 root / posed / hook / 调试器 / 恶意输入法 等环境下,密码仍可能泄露" +
|
||||||
"\n3. 本功能不会绕过系统锁屏认证,仅用于你已合法授权控制的设备" +
|
"\n3. 本功能不会绕过系统锁屏认证,仅用于你已合法授权控制的设备" +
|
||||||
"\n4. 关闭“填充密码时需要验证”会显著降低安全性,请谨慎选择",
|
"\n4. 关闭“填充密码时需要验证”会显著降低安全性,请谨慎选择",
|
||||||
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
fontSize = textStyles.body2.fontSize,
|
||||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
color = colorScheme.onSurfaceVariantSummary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = UiSpacing.Large)
|
.padding(top = UiSpacing.Large)
|
||||||
.padding(horizontal = UiSpacing.Large),
|
.padding(horizontal = UiSpacing.Large),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { Spacer(Modifier.height(UiSpacing.PageBottom)) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,7 +553,6 @@ private fun PasswordMenuPopup(
|
|||||||
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
|
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
|
||||||
alignment = PopupPositionProvider.Align.TopEnd,
|
alignment = PopupPositionProvider.Align.TopEnd,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
enableWindowDim = false,
|
|
||||||
) {
|
) {
|
||||||
ListPopupColumn {
|
ListPopupColumn {
|
||||||
SpinnerItemImpl(
|
SpinnerItemImpl(
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid
|
package io.github.miuzarte.scrcpyforandroid
|
||||||
|
|
||||||
import android.os.Build
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import io.github.miuzarte.scrcpyforandroid.pages.MainScreen
|
import io.github.miuzarte.scrcpyforandroid.pages.MainScreen
|
||||||
import io.github.miuzarte.scrcpyforandroid.password.BiometricGate
|
import io.github.miuzarte.scrcpyforandroid.password.BiometricGate
|
||||||
@@ -13,12 +12,10 @@ import io.github.miuzarte.scrcpyforandroid.password.hasAuthenticatedOrigin
|
|||||||
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.AppWakeLocks
|
import io.github.miuzarte.scrcpyforandroid.services.AppWakeLocks
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration
|
import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
// 生物认证需要 FragmentActivity
|
// 生物认证需要 FragmentActivity
|
||||||
class MainActivity : FragmentActivity() {
|
class MainActivity : FragmentActivity() {
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ class StreamActivity : FragmentActivity() {
|
|||||||
//+ onPictureInPictureUiStateChanged
|
//+ onPictureInPictureUiStateChanged
|
||||||
//- onUserLeaveHint
|
//- onUserLeaveHint
|
||||||
|
|
||||||
// @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
|
||||||
override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
|
override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
|
||||||
super.onPictureInPictureUiStateChanged(pipState)
|
super.onPictureInPictureUiStateChanged(pipState)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.nativecore
|
package io.github.miuzarte.scrcpyforandroid.nativecore
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.nsd.NsdManager
|
import android.net.nsd.NsdManager
|
||||||
import android.net.nsd.NsdServiceInfo
|
import android.net.nsd.NsdServiceInfo
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
@@ -15,7 +14,6 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
|
||||||
/**
|
/**
|
||||||
* Performs mDNS discovery for ADB TLS pairing/connect services on the local network.
|
* Performs mDNS discovery for ADB TLS pairing/connect services on the local network.
|
||||||
*
|
*
|
||||||
@@ -26,7 +24,7 @@ internal object AdbMdnsDiscoverer {
|
|||||||
|
|
||||||
private lateinit var nsdManager: NsdManager
|
private lateinit var nsdManager: NsdManager
|
||||||
|
|
||||||
fun init(context: android.content.Context) {
|
fun init(context: Context) {
|
||||||
if (::nsdManager.isInitialized) return
|
if (::nsdManager.isInitialized) return
|
||||||
nsdManager = context.applicationContext.getSystemService(NsdManager::class.java)
|
nsdManager = context.applicationContext.getSystemService(NsdManager::class.java)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.nativecore
|
package io.github.miuzarte.scrcpyforandroid.nativecore
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
@@ -131,7 +129,6 @@ private class PairingContext private constructor(private val nativePtr: Long) {
|
|||||||
* keying material using the user-supplied pairing code, and exchanges peer
|
* keying material using the user-supplied pairing code, and exchanges peer
|
||||||
* information to complete pairing.
|
* information to complete pairing.
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
|
||||||
internal class DirectAdbPairingClient(
|
internal class DirectAdbPairingClient(
|
||||||
private val host: String,
|
private val host: String,
|
||||||
private val port: Int,
|
private val port: Int,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.pages
|
package io.github.miuzarte.scrcpyforandroid.pages
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -13,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
@@ -23,15 +23,17 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.runtime.withFrameNanos
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.luminance
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.layout.positionInWindow
|
import androidx.compose.ui.layout.positionInWindow
|
||||||
@@ -45,7 +47,10 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import io.github.miuzarte.scrcpyforandroid.BuildConfig
|
import io.github.miuzarte.scrcpyforandroid.BuildConfig
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.pages.effect.BgEffectBackground
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker
|
import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import top.yukonga.miuix.kmp.basic.Card
|
import top.yukonga.miuix.kmp.basic.Card
|
||||||
import top.yukonga.miuix.kmp.basic.CardDefaults
|
import top.yukonga.miuix.kmp.basic.CardDefaults
|
||||||
@@ -60,22 +65,20 @@ import top.yukonga.miuix.kmp.blur.BlurBlendMode
|
|||||||
import top.yukonga.miuix.kmp.blur.BlurColors
|
import top.yukonga.miuix.kmp.blur.BlurColors
|
||||||
import top.yukonga.miuix.kmp.blur.BlurDefaults
|
import top.yukonga.miuix.kmp.blur.BlurDefaults
|
||||||
import top.yukonga.miuix.kmp.blur.LayerBackdrop
|
import top.yukonga.miuix.kmp.blur.LayerBackdrop
|
||||||
import top.yukonga.miuix.kmp.blur.RuntimeShader
|
|
||||||
import top.yukonga.miuix.kmp.blur.asBrush
|
|
||||||
import top.yukonga.miuix.kmp.blur.isRenderEffectSupported
|
import top.yukonga.miuix.kmp.blur.isRenderEffectSupported
|
||||||
import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported
|
|
||||||
import top.yukonga.miuix.kmp.blur.layerBackdrop
|
import top.yukonga.miuix.kmp.blur.layerBackdrop
|
||||||
import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop
|
import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop
|
||||||
import top.yukonga.miuix.kmp.blur.textureBlur
|
import top.yukonga.miuix.kmp.blur.textureBlur
|
||||||
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
||||||
import top.yukonga.miuix.kmp.shapes.SmoothRoundedCornerShape
|
import top.yukonga.miuix.kmp.shapes.SmoothRoundedCornerShape
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
import androidx.compose.ui.graphics.BlendMode as ComposeBlendMode
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AboutScreen() {
|
internal fun AboutScreen() {
|
||||||
val navigator = LocalRootNavigator.current
|
val navigator = LocalRootNavigator.current
|
||||||
|
val enableBlur = LocalEnableBlur.current
|
||||||
|
val blurBackdrop = rememberBlurBackdrop(enableBlur)
|
||||||
val topAppBarScrollBehavior = MiuixScrollBehavior()
|
val topAppBarScrollBehavior = MiuixScrollBehavior()
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
var logoHeightPx by remember { mutableIntStateOf(0) }
|
var logoHeightPx by remember { mutableIntStateOf(0) }
|
||||||
@@ -101,7 +104,12 @@ internal fun AboutScreen() {
|
|||||||
SmallTopAppBar(
|
SmallTopAppBar(
|
||||||
title = "关于",
|
title = "关于",
|
||||||
scrollBehavior = topAppBarScrollBehavior,
|
scrollBehavior = topAppBarScrollBehavior,
|
||||||
color = colorScheme.surface.copy(alpha = if (scrollProgress == 1f) 1f else 0f),
|
modifier =
|
||||||
|
if (blurBackdrop != null) Modifier.layerBackdrop(blurBackdrop)
|
||||||
|
else Modifier,
|
||||||
|
color =
|
||||||
|
if (blurBackdrop != null) Color.Transparent
|
||||||
|
else colorScheme.surface.copy(alpha = if (scrollProgress == 1f) 1f else 0f),
|
||||||
titleColor = colorScheme.onSurface.copy(alpha = scrollProgress),
|
titleColor = colorScheme.onSurface.copy(alpha = scrollProgress),
|
||||||
defaultWindowInsetsPadding = false,
|
defaultWindowInsetsPadding = false,
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
@@ -120,6 +128,7 @@ internal fun AboutScreen() {
|
|||||||
top = innerPadding.calculateTopPadding(),
|
top = innerPadding.calculateTopPadding(),
|
||||||
bottom = innerPadding.calculateBottomPadding(),
|
bottom = innerPadding.calculateBottomPadding(),
|
||||||
),
|
),
|
||||||
|
enableBlur = enableBlur,
|
||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
scrollProgress = scrollProgress,
|
scrollProgress = scrollProgress,
|
||||||
onLogoHeightChanged = { logoHeightPx = it },
|
onLogoHeightChanged = { logoHeightPx = it },
|
||||||
@@ -130,16 +139,20 @@ internal fun AboutScreen() {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun AboutContent(
|
private fun AboutContent(
|
||||||
padding: PaddingValues,
|
padding: PaddingValues,
|
||||||
lazyListState: androidx.compose.foundation.lazy.LazyListState,
|
enableBlur: Boolean,
|
||||||
|
lazyListState: LazyListState,
|
||||||
scrollProgress: Float,
|
scrollProgress: Float,
|
||||||
onLogoHeightChanged: (Int) -> Unit,
|
onLogoHeightChanged: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val updateState by AppUpdateChecker.state.collectAsState()
|
val updateState by AppUpdateChecker.state.collectAsState()
|
||||||
val backdrop = rememberLayerBackdrop()
|
val backdrop = rememberLayerBackdrop()
|
||||||
val blurEnabled = remember { isRenderEffectSupported() }
|
var isOs3Effect by remember { mutableStateOf(true) }
|
||||||
val isDark = MiuixTheme.colorScheme.background.luminance() < 0.5f
|
val blurEnabled = remember(enableBlur) { enableBlur && isRenderEffectSupported() }
|
||||||
|
val isDark = colorScheme.background.luminance() < 0.5f
|
||||||
|
val heroBlendColors = remember(isDark) { heroBlendColors(isDark) }
|
||||||
val cardBlendColors = remember(isDark) { aboutCardBlendToken(isDark) }
|
val cardBlendColors = remember(isDark) { aboutCardBlendToken(isDark) }
|
||||||
|
|
||||||
var logoAreaY by remember { mutableFloatStateOf(0f) }
|
var logoAreaY by remember { mutableFloatStateOf(0f) }
|
||||||
var projectNameY by remember { mutableFloatStateOf(0f) }
|
var projectNameY by remember { mutableFloatStateOf(0f) }
|
||||||
var versionCodeY by remember { mutableFloatStateOf(0f) }
|
var versionCodeY by remember { mutableFloatStateOf(0f) }
|
||||||
@@ -172,12 +185,12 @@ private fun AboutContent(
|
|||||||
val stage2TotalLength = versionCodeY - projectNameY
|
val stage2TotalLength = versionCodeY - projectNameY
|
||||||
val versionCodeDelay = stage1TotalLength * 0.5f
|
val versionCodeDelay = stage1TotalLength * 0.5f
|
||||||
versionCodeProgress =
|
versionCodeProgress =
|
||||||
((offset.toFloat() - versionCodeDelay) / (stage1TotalLength - versionCodeDelay).coerceAtLeast(
|
((offset.toFloat() - versionCodeDelay) / (stage1TotalLength - versionCodeDelay)
|
||||||
1f
|
.coerceAtLeast(1f))
|
||||||
))
|
|
||||||
.coerceIn(0f, 1f)
|
.coerceIn(0f, 1f)
|
||||||
projectNameProgress =
|
projectNameProgress =
|
||||||
((offset.toFloat() - stage1TotalLength) / stage2TotalLength.coerceAtLeast(1f))
|
((offset.toFloat() - stage1TotalLength) / stage2TotalLength
|
||||||
|
.coerceAtLeast(1f))
|
||||||
.coerceIn(0f, 1f)
|
.coerceIn(0f, 1f)
|
||||||
}
|
}
|
||||||
.collect {}
|
.collect {}
|
||||||
@@ -186,8 +199,11 @@ private fun AboutContent(
|
|||||||
BgEffectBackground(
|
BgEffectBackground(
|
||||||
dynamicBackground = true,
|
dynamicBackground = true,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
bgModifier = Modifier.layerBackdrop(backdrop),
|
bgModifier =
|
||||||
|
if (blurEnabled) Modifier.layerBackdrop(backdrop)
|
||||||
|
else Modifier,
|
||||||
effectBackground = true,
|
effectBackground = true,
|
||||||
|
isOs3Effect = isOs3Effect,
|
||||||
alpha = { 1f - scrollProgress },
|
alpha = { 1f - scrollProgress },
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -206,12 +222,12 @@ private fun AboutContent(
|
|||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val textMeasurer = rememberTextMeasurer()
|
val textMeasurer = rememberTextMeasurer()
|
||||||
val baseTitleFontSize = 35.sp
|
val baseTitleFontSize = 32.sp
|
||||||
val titleLayout = remember(textMeasurer) {
|
val titleLayout = remember(textMeasurer) {
|
||||||
textMeasurer.measure(
|
textMeasurer.measure(
|
||||||
text = "Scrcpy for Android",
|
text = "Scrcpy for Android",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Black,
|
||||||
fontSize = baseTitleFontSize,
|
fontSize = baseTitleFontSize,
|
||||||
),
|
),
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
@@ -226,7 +242,7 @@ private fun AboutContent(
|
|||||||
Text(
|
Text(
|
||||||
text = "Scrcpy for Android",
|
text = "Scrcpy for Android",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 12.dp, bottom = 5.dp)
|
.padding(top = 12.dp, bottom = 6.dp)
|
||||||
.onGloballyPositioned { coordinates ->
|
.onGloballyPositioned { coordinates ->
|
||||||
if (projectNameY != 0f) return@onGloballyPositioned
|
if (projectNameY != 0f) return@onGloballyPositioned
|
||||||
val y = coordinates.positionInWindow().y
|
val y = coordinates.positionInWindow().y
|
||||||
@@ -243,13 +259,11 @@ private fun AboutContent(
|
|||||||
shape = SmoothRoundedCornerShape(16.dp),
|
shape = SmoothRoundedCornerShape(16.dp),
|
||||||
blurRadius = 150f,
|
blurRadius = 150f,
|
||||||
noiseCoefficient = BlurDefaults.NoiseCoefficient,
|
noiseCoefficient = BlurDefaults.NoiseCoefficient,
|
||||||
colors = BlurColors(
|
colors = BlurColors(blendColors = heroBlendColors),
|
||||||
blendColors = heroBlendColors(isDark),
|
contentBlendMode = BlendMode.DstIn,
|
||||||
),
|
|
||||||
contentBlendMode = ComposeBlendMode.DstIn,
|
|
||||||
enabled = blurEnabled,
|
enabled = blurEnabled,
|
||||||
),
|
),
|
||||||
color = MiuixTheme.colorScheme.onBackground,
|
color = colorScheme.onBackground,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = titleFontSize,
|
fontSize = titleFontSize,
|
||||||
)
|
)
|
||||||
@@ -269,7 +283,7 @@ private fun AboutContent(
|
|||||||
val size = coordinates.size
|
val size = coordinates.size
|
||||||
versionCodeY = y + size.height
|
versionCodeY = y + size.height
|
||||||
},
|
},
|
||||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
color = colorScheme.onSurfaceVariantSummary,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
@@ -288,7 +302,7 @@ private fun AboutContent(
|
|||||||
val size = coordinates.size
|
val size = coordinates.size
|
||||||
versionCodeY = y + size.height
|
versionCodeY = y + size.height
|
||||||
},
|
},
|
||||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
color = colorScheme.onSurfaceVariantSummary,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
@@ -309,6 +323,13 @@ private fun AboutContent(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(320.dp)
|
.height(320.dp)
|
||||||
.padding(top = 36.dp)
|
.padding(top = 36.dp)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onDoubleTap = {
|
||||||
|
isOs3Effect = !isOs3Effect
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
.onSizeChanged { onLogoHeightChanged(it.height) }
|
.onSizeChanged { onLogoHeightChanged(it.height) }
|
||||||
.onGloballyPositioned { coordinates ->
|
.onGloballyPositioned { coordinates ->
|
||||||
val y = coordinates.positionInWindow().y
|
val y = coordinates.positionInWindow().y
|
||||||
@@ -336,7 +357,7 @@ private fun AboutContent(
|
|||||||
endActions = {
|
endActions = {
|
||||||
Text(
|
Text(
|
||||||
text = "GitHub",
|
text = "GitHub",
|
||||||
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
fontSize = textStyles.body2.fontSize,
|
||||||
color = colorScheme.onSurfaceVariantActions,
|
color = colorScheme.onSurfaceVariantActions,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -352,7 +373,7 @@ private fun AboutContent(
|
|||||||
releaseStatusText?.let {
|
releaseStatusText?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
fontSize = textStyles.body2.fontSize,
|
||||||
color = colorScheme.onSurfaceVariantActions,
|
color = colorScheme.onSurfaceVariantActions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -375,7 +396,7 @@ private fun AboutContent(
|
|||||||
endActions = {
|
endActions = {
|
||||||
Text(
|
Text(
|
||||||
text = "Apache-2.0",
|
text = "Apache-2.0",
|
||||||
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
fontSize = textStyles.body2.fontSize,
|
||||||
color = colorScheme.onSurfaceVariantActions,
|
color = colorScheme.onSurfaceVariantActions,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -413,335 +434,35 @@ private fun AboutCard(
|
|||||||
enabled = blurEnabled,
|
enabled = blurEnabled,
|
||||||
),
|
),
|
||||||
colors = CardDefaults.defaultColors(
|
colors = CardDefaults.defaultColors(
|
||||||
if (blurEnabled) Color.Transparent else MiuixTheme.colorScheme.surfaceContainer,
|
color =
|
||||||
Color.Transparent,
|
if (blurEnabled) Color.Transparent
|
||||||
|
else colorScheme.surfaceContainer,
|
||||||
|
contentColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun heroBlendColors(isDark: Boolean): List<BlendColorEntry> =
|
private fun heroBlendColors(isDark: Boolean): List<BlendColorEntry> =
|
||||||
remember(isDark) {
|
if (isDark) listOf(
|
||||||
if (isDark) {
|
BlendColorEntry(Color(0xE6A1A1A1), BlurBlendMode.ColorDodge),
|
||||||
listOf(
|
BlendColorEntry(Color(0x4DE6E6E6), BlurBlendMode.LinearLight),
|
||||||
BlendColorEntry(Color(0xE6A1A1A1), BlurBlendMode.ColorDodge),
|
BlendColorEntry(Color(0xFF1AF500), BlurBlendMode.Lab),
|
||||||
BlendColorEntry(Color(0x4DE6E6E6), BlurBlendMode.LinearLight),
|
)
|
||||||
BlendColorEntry(Color(0xFF1AF500), BlurBlendMode.Lab),
|
else listOf(
|
||||||
)
|
BlendColorEntry(Color(0xCC4A4A4A), BlurBlendMode.ColorBurn),
|
||||||
} else {
|
BlendColorEntry(Color(0xFF4F4F4F), BlurBlendMode.LinearLight),
|
||||||
listOf(
|
BlendColorEntry(Color(0xFF1AF200), BlurBlendMode.Lab),
|
||||||
BlendColorEntry(Color(0xCC4A4A4A), BlurBlendMode.ColorBurn),
|
)
|
||||||
BlendColorEntry(Color(0xFF4F4F4F), BlurBlendMode.LinearLight),
|
|
||||||
BlendColorEntry(Color(0xFF1AF200), BlurBlendMode.Lab),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun aboutCardBlendToken(isDark: Boolean): List<BlendColorEntry> =
|
private fun aboutCardBlendToken(isDark: Boolean): List<BlendColorEntry> =
|
||||||
if (isDark) {
|
if (isDark) listOf(
|
||||||
listOf(
|
BlendColorEntry(Color(0x4DA9A9A9), BlurBlendMode.Luminosity),
|
||||||
BlendColorEntry(Color(0x4DA9A9A9), BlurBlendMode.Luminosity),
|
BlendColorEntry(Color(0x1A9C9C9C), BlurBlendMode.PlusDarker),
|
||||||
BlendColorEntry(Color(0x1A9C9C9C), BlurBlendMode.PlusDarker),
|
)
|
||||||
)
|
else listOf(
|
||||||
} else {
|
BlendColorEntry(Color(0x340034F9), BlurBlendMode.Overlay),
|
||||||
listOf(
|
BlendColorEntry(Color(0xB3FFFFFF), BlurBlendMode.HardLight),
|
||||||
BlendColorEntry(Color(0x340034F9), BlurBlendMode.Overlay),
|
)
|
||||||
BlendColorEntry(Color(0xB3FFFFFF), BlurBlendMode.HardLight),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private inline fun BgEffectBackground(
|
|
||||||
dynamicBackground: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
bgModifier: Modifier = Modifier,
|
|
||||||
effectBackground: Boolean = true,
|
|
||||||
crossinline alpha: () -> Float = { 1f },
|
|
||||||
content: @Composable BoxScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
val shaderSupported = remember { isRuntimeShaderSupported() }
|
|
||||||
if (!shaderSupported) {
|
|
||||||
Box(modifier = modifier, content = content)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Box(modifier = modifier) {
|
|
||||||
val surface = colorScheme.surface
|
|
||||||
val painter = remember { BgEffectPainter() }
|
|
||||||
val animTime = rememberFrameTimeSeconds(dynamicBackground)
|
|
||||||
val isDark = colorScheme.background.luminance() < 0.5f
|
|
||||||
|
|
||||||
Canvas(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.then(bgModifier),
|
|
||||||
) {
|
|
||||||
drawRect(surface)
|
|
||||||
if (effectBackground) {
|
|
||||||
val drawHeight = size.height * 0.78f
|
|
||||||
painter.updateResolution(size.width, size.height)
|
|
||||||
painter.updatePresetIfNeeded(drawHeight, size.height, size.width, isDark)
|
|
||||||
painter.updateAnimTime(animTime())
|
|
||||||
drawRect(painter.runtimeShader.asBrush(), alpha = alpha())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun rememberFrameTimeSeconds(
|
|
||||||
playing: Boolean = true,
|
|
||||||
): () -> Float {
|
|
||||||
var time by remember { mutableFloatStateOf(0f) }
|
|
||||||
var startOffset by remember { mutableFloatStateOf(0f) }
|
|
||||||
|
|
||||||
LaunchedEffect(playing) {
|
|
||||||
if (!playing) {
|
|
||||||
startOffset = time
|
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
|
||||||
val start = withFrameNanos { it }
|
|
||||||
while (playing) {
|
|
||||||
val now = withFrameNanos { it }
|
|
||||||
time = startOffset + (now - start) / 1_000_000_000f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { time }
|
|
||||||
}
|
|
||||||
|
|
||||||
// @RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
|
||||||
private class BgEffectPainter {
|
|
||||||
val runtimeShader by lazy {
|
|
||||||
RuntimeShader(OS2_BG_FRAG).also { initStaticUniforms(it) }
|
|
||||||
}
|
|
||||||
private val resolution = FloatArray(2)
|
|
||||||
private val bound = FloatArray(4)
|
|
||||||
private var animTime = Float.NaN
|
|
||||||
private var isDarkCached: Boolean? = null
|
|
||||||
private var presetApplied = false
|
|
||||||
|
|
||||||
private fun initStaticUniforms(shader: RuntimeShader) {
|
|
||||||
shader.setFloatUniform("uTranslateY", 0f)
|
|
||||||
shader.setFloatUniform("uNoiseScale", 1.5f)
|
|
||||||
shader.setFloatUniform("uPointOffset", 0.1f)
|
|
||||||
shader.setFloatUniform("uPointRadiusMulti", 1f)
|
|
||||||
shader.setFloatUniform("uAlphaMulti", 1f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateResolution(width: Float, height: Float) {
|
|
||||||
if (resolution[0] == width && resolution[1] == height) return
|
|
||||||
resolution[0] = width
|
|
||||||
resolution[1] = height
|
|
||||||
runtimeShader.setFloatUniform("uResolution", resolution)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateAnimTime(time: Float) {
|
|
||||||
if (animTime == time) return
|
|
||||||
animTime = time
|
|
||||||
runtimeShader.setFloatUniform("uAnimTime", animTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updatePresetIfNeeded(
|
|
||||||
logoHeight: Float,
|
|
||||||
height: Float,
|
|
||||||
width: Float,
|
|
||||||
isDark: Boolean,
|
|
||||||
) {
|
|
||||||
if (presetApplied && isDarkCached == isDark) return
|
|
||||||
updateBound(logoHeight, height, width)
|
|
||||||
val preset = if (isDark) PHONE_DARK else PHONE_LIGHT
|
|
||||||
runtimeShader.setFloatUniform("uPoints", preset.points)
|
|
||||||
runtimeShader.setFloatUniform("uColors", preset.colors)
|
|
||||||
runtimeShader.setFloatUniform("uLightOffset", preset.lightOffset)
|
|
||||||
runtimeShader.setFloatUniform("uSaturateOffset", preset.saturateOffset)
|
|
||||||
runtimeShader.setFloatUniform("uBound", bound)
|
|
||||||
isDarkCached = isDark
|
|
||||||
presetApplied = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateBound(
|
|
||||||
logoHeight: Float,
|
|
||||||
totalHeight: Float,
|
|
||||||
totalWidth: Float,
|
|
||||||
) {
|
|
||||||
val heightRatio = logoHeight / totalHeight
|
|
||||||
if (totalWidth <= totalHeight) {
|
|
||||||
bound[0] = 0f
|
|
||||||
bound[1] = 1f - heightRatio
|
|
||||||
bound[2] = 1f
|
|
||||||
bound[3] = heightRatio
|
|
||||||
} else {
|
|
||||||
val aspectRatio = totalWidth / totalHeight
|
|
||||||
val contentCenterY = 1f - heightRatio / 2f
|
|
||||||
bound[0] = 0f
|
|
||||||
bound[1] = contentCenterY - aspectRatio / 2f
|
|
||||||
bound[2] = 1f
|
|
||||||
bound[3] = aspectRatio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class BgEffectPreset(
|
|
||||||
val points: FloatArray,
|
|
||||||
val colors: FloatArray,
|
|
||||||
val lightOffset: Float,
|
|
||||||
val saturateOffset: Float,
|
|
||||||
) {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as BgEffectPreset
|
|
||||||
|
|
||||||
if (lightOffset != other.lightOffset) return false
|
|
||||||
if (saturateOffset != other.saturateOffset) return false
|
|
||||||
if (!points.contentEquals(other.points)) return false
|
|
||||||
if (!colors.contentEquals(other.colors)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = lightOffset.hashCode()
|
|
||||||
result = 31 * result + saturateOffset.hashCode()
|
|
||||||
result = 31 * result + points.contentHashCode()
|
|
||||||
result = 31 * result + colors.contentHashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
val PHONE_LIGHT = BgEffectPreset(
|
|
||||||
points = floatArrayOf(
|
|
||||||
0.67f, 0.42f, 1.0f, 0.69f, 0.75f, 1.0f,
|
|
||||||
0.14f, 0.71f, 0.95f, 0.14f, 0.27f, 0.8f,
|
|
||||||
),
|
|
||||||
colors = floatArrayOf(
|
|
||||||
0.57f, 0.76f, 0.98f, 1.0f,
|
|
||||||
0.98f, 0.85f, 0.68f, 1.0f,
|
|
||||||
0.98f, 0.75f, 0.93f, 1.0f,
|
|
||||||
0.73f, 0.70f, 0.98f, 1.0f,
|
|
||||||
),
|
|
||||||
lightOffset = 0.1f,
|
|
||||||
saturateOffset = 0.2f,
|
|
||||||
)
|
|
||||||
val PHONE_DARK = BgEffectPreset(
|
|
||||||
points = floatArrayOf(
|
|
||||||
0.63f, 0.50f, 0.88f, 0.69f, 0.75f, 0.80f,
|
|
||||||
0.17f, 0.66f, 0.81f, 0.14f, 0.24f, 0.72f,
|
|
||||||
),
|
|
||||||
colors = floatArrayOf(
|
|
||||||
0.0f, 0.31f, 0.58f, 1.0f,
|
|
||||||
0.53f, 0.29f, 0.15f, 1.0f,
|
|
||||||
0.46f, 0.06f, 0.27f, 1.0f,
|
|
||||||
0.16f, 0.12f, 0.45f, 1.0f,
|
|
||||||
),
|
|
||||||
lightOffset = -0.1f,
|
|
||||||
saturateOffset = 0.2f,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val OS2_BG_FRAG = """
|
|
||||||
uniform vec2 uResolution;
|
|
||||||
uniform shader uTex;
|
|
||||||
uniform float uAnimTime;
|
|
||||||
uniform vec4 uBound;
|
|
||||||
uniform float uTranslateY;
|
|
||||||
uniform vec3 uPoints[4];
|
|
||||||
uniform vec4 uColors[4];
|
|
||||||
uniform float uAlphaMulti;
|
|
||||||
uniform float uNoiseScale;
|
|
||||||
uniform float uPointOffset;
|
|
||||||
uniform float uPointRadiusMulti;
|
|
||||||
uniform float uSaturateOffset;
|
|
||||||
uniform float uLightOffset;
|
|
||||||
|
|
||||||
vec3 rgb2hsv(vec3 c)
|
|
||||||
{
|
|
||||||
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
|
||||||
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
|
|
||||||
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
|
|
||||||
float d = q.x - min(q.w, q.y);
|
|
||||||
float e = 1.0e-10;
|
|
||||||
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
vec3 hsv2rgb(vec3 c)
|
|
||||||
{
|
|
||||||
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
|
||||||
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
|
||||||
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
float hash(vec2 p) {
|
|
||||||
vec3 p3 = fract(vec3(p.xyx) * 0.13);
|
|
||||||
p3 += dot(p3, p3.yzx + 3.333);
|
|
||||||
return fract((p3.x + p3.y) * p3.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
float perlin(vec2 x) {
|
|
||||||
vec2 i = floor(x);
|
|
||||||
vec2 f = fract(x);
|
|
||||||
float a = hash(i);
|
|
||||||
float b = hash(i + vec2(1.0, 0.0));
|
|
||||||
float c = hash(i + vec2(0.0, 1.0));
|
|
||||||
float d = hash(i + vec2(1.0, 1.0));
|
|
||||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
||||||
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
float gradientNoise(in vec2 uv)
|
|
||||||
{
|
|
||||||
return fract(52.9829189 * fract(dot(uv, vec2(0.06711056, 0.00583715))));
|
|
||||||
}
|
|
||||||
|
|
||||||
vec4 main(vec2 fragCoord){
|
|
||||||
vec2 vUv = fragCoord/uResolution;
|
|
||||||
vUv.y = 1.0-vUv.y;
|
|
||||||
vec2 uv = vUv;
|
|
||||||
uv -= vec2(0., uTranslateY);
|
|
||||||
uv.xy -= uBound.xy;
|
|
||||||
uv.xy /= uBound.zw;
|
|
||||||
vec3 hsv;
|
|
||||||
vec4 color = vec4(0.0);
|
|
||||||
float noiseValue = perlin(vUv * uNoiseScale + vec2(-uAnimTime, -uAnimTime));
|
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++){
|
|
||||||
vec4 pointColor = uColors[i];
|
|
||||||
pointColor.rgb *= pointColor.a;
|
|
||||||
vec2 point = uPoints[i].xy;
|
|
||||||
float rad = uPoints[i].z * uPointRadiusMulti;
|
|
||||||
point.x += sin(uAnimTime + point.y) * uPointOffset;
|
|
||||||
point.y += cos(uAnimTime + point.x) * uPointOffset;
|
|
||||||
float d = distance(uv, point);
|
|
||||||
float pct = smoothstep(rad, 0., d);
|
|
||||||
color.rgb = mix(color.rgb, pointColor.rgb, pct);
|
|
||||||
color.a = mix(color.a, pointColor.a, pct);
|
|
||||||
}
|
|
||||||
|
|
||||||
float oppositeNoise = smoothstep(0., 1., noiseValue);
|
|
||||||
if (color.a > 0.001) {
|
|
||||||
color.rgb /= color.a;
|
|
||||||
} else {
|
|
||||||
color.rgb = vec3(0.0);
|
|
||||||
}
|
|
||||||
hsv = rgb2hsv(color.rgb);
|
|
||||||
hsv.y = mix(hsv.y, 0.0, oppositeNoise * uSaturateOffset);
|
|
||||||
color.rgb = hsv2rgb(hsv);
|
|
||||||
color.rgb += oppositeNoise * uLightOffset;
|
|
||||||
color.a = clamp(color.a, 0., 1.);
|
|
||||||
color.a *= uAlphaMulti;
|
|
||||||
|
|
||||||
vec4 uiColor = uTex.eval(vec2(vUv.x, 1.0 - vUv.y)*uResolution);
|
|
||||||
vec4 fragColor;
|
|
||||||
color.rgb += (10.0 / 255.0) * gradientNoise(fragCoord.xy) - (5.0 / 255.0);
|
|
||||||
if (uiColor.a < 0.01) {
|
|
||||||
fragColor = color;
|
|
||||||
} else {
|
|
||||||
fragColor = uiColor;
|
|
||||||
}
|
|
||||||
return vec4(fragColor.rgb*fragColor.a, fragColor.a);
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.activity.compose.LocalActivity
|
|||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -40,6 +41,7 @@ import androidx.compose.ui.graphics.BlendModeColorFilter
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@@ -70,6 +72,9 @@ import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
|||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyProfiles
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyProfiles
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel
|
import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTileList
|
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTileList
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.PairingCard
|
import io.github.miuzarte.scrcpyforandroid.widgets.PairingCard
|
||||||
@@ -102,12 +107,14 @@ import top.yukonga.miuix.kmp.basic.SpinnerEntry
|
|||||||
import top.yukonga.miuix.kmp.basic.Text
|
import top.yukonga.miuix.kmp.basic.Text
|
||||||
import top.yukonga.miuix.kmp.basic.TextField
|
import top.yukonga.miuix.kmp.basic.TextField
|
||||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||||
|
import top.yukonga.miuix.kmp.blur.layerBackdrop
|
||||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||||
import top.yukonga.miuix.kmp.icon.basic.Check
|
import top.yukonga.miuix.kmp.icon.basic.Check
|
||||||
import top.yukonga.miuix.kmp.icon.extended.Store
|
import top.yukonga.miuix.kmp.icon.extended.Store
|
||||||
import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet
|
import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet
|
||||||
import top.yukonga.miuix.kmp.overlay.OverlayListPopup
|
import top.yukonga.miuix.kmp.overlay.OverlayListPopup
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
|
||||||
|
|
||||||
private const val ADB_CONNECT_TIMEOUT_MS = 12_000L
|
private const val ADB_CONNECT_TIMEOUT_MS = 12_000L
|
||||||
private const val ADB_KEEPALIVE_INTERVAL_MS = 3_000L
|
private const val ADB_KEEPALIVE_INTERVAL_MS = 3_000L
|
||||||
@@ -129,52 +136,63 @@ private data class StartAppRequest(
|
|||||||
fun DeviceTabScreen(
|
fun DeviceTabScreen(
|
||||||
scrollBehavior: ScrollBehavior,
|
scrollBehavior: ScrollBehavior,
|
||||||
scrcpy: Scrcpy,
|
scrcpy: Scrcpy,
|
||||||
|
bottomInnerPadding: Dp,
|
||||||
onOpenReorderDevices: () -> Unit,
|
onOpenReorderDevices: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val navigator = LocalRootNavigator.current
|
val navigator = LocalRootNavigator.current
|
||||||
var showThreePointMenu by rememberSaveable { mutableStateOf(false) }
|
var showThreePointMenu by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current)
|
||||||
|
val blurActive = blurBackdrop != null
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
BlurredBar(backdrop = blurBackdrop) {
|
||||||
title = "设备",
|
TopAppBar(
|
||||||
actions = {
|
title = "设备",
|
||||||
IconButton(
|
color =
|
||||||
onClick = { showThreePointMenu = true },
|
if (blurActive) Color.Transparent
|
||||||
holdDownState = showThreePointMenu,
|
else colorScheme.surface,
|
||||||
) {
|
actions = {
|
||||||
Icon(
|
IconButton(
|
||||||
Icons.Rounded.MoreVert,
|
onClick = { showThreePointMenu = true },
|
||||||
contentDescription = "更多"
|
holdDownState = showThreePointMenu,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.MoreVert,
|
||||||
|
contentDescription = "更多"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DeviceMenuPopup(
|
||||||
|
show = showThreePointMenu,
|
||||||
|
onDismissRequest = { showThreePointMenu = false },
|
||||||
|
onReorderDevices = {
|
||||||
|
onOpenReorderDevices()
|
||||||
|
showThreePointMenu = false
|
||||||
|
},
|
||||||
|
onOpenVirtualButtonOrder = {
|
||||||
|
navigator.push(RootScreen.VirtualButtonOrder)
|
||||||
|
showThreePointMenu = false
|
||||||
|
},
|
||||||
|
canClearLogs = EventLogger.hasLogs(),
|
||||||
|
onClearLogs = {
|
||||||
|
EventLogger.clearLogs()
|
||||||
|
showThreePointMenu = false
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
DeviceMenuPopup(
|
scrollBehavior = scrollBehavior,
|
||||||
show = showThreePointMenu,
|
)
|
||||||
onDismissRequest = { showThreePointMenu = false },
|
}
|
||||||
onReorderDevices = {
|
|
||||||
onOpenReorderDevices()
|
|
||||||
showThreePointMenu = false
|
|
||||||
},
|
|
||||||
onOpenVirtualButtonOrder = {
|
|
||||||
navigator.push(RootScreen.VirtualButtonOrder)
|
|
||||||
showThreePointMenu = false
|
|
||||||
},
|
|
||||||
canClearLogs = EventLogger.hasLogs(),
|
|
||||||
onClearLogs = {
|
|
||||||
EventLogger.clearLogs()
|
|
||||||
showThreePointMenu = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
) { pagePadding ->
|
) { pagePadding ->
|
||||||
DeviceTabPage(
|
Box(modifier = if (blurActive) Modifier.layerBackdrop(blurBackdrop) else Modifier) {
|
||||||
contentPadding = pagePadding,
|
DeviceTabPage(
|
||||||
scrollBehavior = scrollBehavior,
|
contentPadding = pagePadding,
|
||||||
scrcpy = scrcpy,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
scrcpy = scrcpy,
|
||||||
|
bottomInnerPadding = bottomInnerPadding,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +201,7 @@ fun DeviceTabPage(
|
|||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
scrollBehavior: ScrollBehavior,
|
scrollBehavior: ScrollBehavior,
|
||||||
scrcpy: Scrcpy,
|
scrcpy: Scrcpy,
|
||||||
|
bottomInnerPadding: Dp,
|
||||||
) {
|
) {
|
||||||
val activity = LocalActivity.current
|
val activity = LocalActivity.current
|
||||||
val fragmentActivity = remember(activity) { activity as? FragmentActivity }
|
val fragmentActivity = remember(activity) { activity as? FragmentActivity }
|
||||||
@@ -943,6 +962,7 @@ fun DeviceTabPage(
|
|||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
state = listState,
|
state = listState,
|
||||||
|
bottomInnerPadding = bottomInnerPadding,
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
StatusCard(
|
StatusCard(
|
||||||
@@ -1073,8 +1093,8 @@ fun DeviceTabPage(
|
|||||||
recentTasks.isNotEmpty() -> recentTasks.size.toString()
|
recentTasks.isNotEmpty() -> recentTasks.size.toString()
|
||||||
else -> "空"
|
else -> "空"
|
||||||
},
|
},
|
||||||
color = MiuixTheme.colorScheme.onSurfaceVariantActions,
|
color = colorScheme.onSurfaceVariantActions,
|
||||||
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
fontSize = textStyles.body2.fontSize,
|
||||||
modifier = Modifier.padding(end = UiSpacing.ContentVertical),
|
modifier = Modifier.padding(end = UiSpacing.ContentVertical),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -1180,7 +1200,6 @@ fun DeviceTabPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item { Spacer(Modifier.height(UiSpacing.PageBottom)) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RecentTasksBottomSheet(
|
RecentTasksBottomSheet(
|
||||||
@@ -1224,7 +1243,6 @@ private fun DeviceMenuPopup(
|
|||||||
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
|
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
|
||||||
alignment = PopupPositionProvider.Align.TopEnd,
|
alignment = PopupPositionProvider.Align.TopEnd,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
enableWindowDim = false,
|
|
||||||
) {
|
) {
|
||||||
ListPopupColumn {
|
ListPopupColumn {
|
||||||
DeviceMenuPopupItem(
|
DeviceMenuPopupItem(
|
||||||
@@ -1376,7 +1394,7 @@ private fun RecentTaskSheetItem(
|
|||||||
entry.title?.let {
|
entry.title?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
fontSize = MiuixTheme.textStyles.body1.fontSize,
|
fontSize = textStyles.body1.fontSize,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = spinnerColors.contentColor,
|
color = spinnerColors.contentColor,
|
||||||
)
|
)
|
||||||
@@ -1384,7 +1402,7 @@ private fun RecentTaskSheetItem(
|
|||||||
entry.summary?.let {
|
entry.summary?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
fontSize = textStyles.body2.fontSize,
|
||||||
color = spinnerColors.summaryColor,
|
color = spinnerColors.summaryColor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1425,9 +1443,9 @@ private fun DeviceMenuPopupItem(
|
|||||||
if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem
|
if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
fontSize = MiuixTheme.textStyles.body1.fontSize,
|
fontSize = textStyles.body1.fontSize,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MiuixTheme.colorScheme.disabledOnSecondaryVariant,
|
color = colorScheme.disabledOnSecondaryVariant,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = UiSpacing.PopupHorizontal)
|
.padding(horizontal = UiSpacing.PopupHorizontal)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.pages
|
package io.github.miuzarte.scrcpyforandroid.pages
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
@@ -38,7 +37,6 @@ import androidx.compose.ui.input.pointer.pointerInteropFilter
|
|||||||
import androidx.compose.ui.layout.boundsInWindow
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import androidx.activity.compose.PredictiveBackHandler
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
@@ -32,13 +36,21 @@ import androidx.compose.runtime.rememberUpdatedState
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import androidx.navigation3.runtime.entryProvider
|
import androidx.navigation3.runtime.entryProvider
|
||||||
import androidx.navigation3.runtime.rememberDecoratedNavEntries
|
import androidx.navigation3.runtime.rememberDecoratedNavEntries
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import io.github.miuzarte.scrcpyforandroid.BuildConfig
|
import io.github.miuzarte.scrcpyforandroid.BuildConfig
|
||||||
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
|
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
|
||||||
@@ -55,6 +67,13 @@ import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
|||||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBar
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableFloatingBottomBarBlur
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBar
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.component.FloatingBottomBarItem
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -62,15 +81,19 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import top.yukonga.miuix.kmp.basic.Icon
|
||||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||||
import top.yukonga.miuix.kmp.basic.NavigationBar
|
import top.yukonga.miuix.kmp.basic.NavigationBar
|
||||||
import top.yukonga.miuix.kmp.basic.NavigationBarItem
|
import top.yukonga.miuix.kmp.basic.NavigationBarItem
|
||||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||||
import top.yukonga.miuix.kmp.basic.SnackbarHost
|
import top.yukonga.miuix.kmp.basic.SnackbarHost
|
||||||
import top.yukonga.miuix.kmp.basic.SnackbarHostState
|
import top.yukonga.miuix.kmp.basic.SnackbarHostState
|
||||||
|
import top.yukonga.miuix.kmp.basic.Text
|
||||||
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
|
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
import top.yukonga.miuix.kmp.theme.ThemeController
|
import top.yukonga.miuix.kmp.theme.ThemeController
|
||||||
|
import top.yukonga.miuix.kmp.blur.layerBackdrop as miuixLayerBackdrop
|
||||||
|
|
||||||
private enum class MainBottomTabDestination(
|
private enum class MainBottomTabDestination(
|
||||||
val label: String,
|
val label: String,
|
||||||
@@ -316,59 +339,151 @@ fun MainScreen() {
|
|||||||
|
|
||||||
val rootEntryProvider = entryProvider<NavKey> {
|
val rootEntryProvider = entryProvider<NavKey> {
|
||||||
entry(RootScreen.Home) {
|
entry(RootScreen.Home) {
|
||||||
|
val blurBackdrop = rememberBlurBackdrop(enableBlur = asBundle.blur)
|
||||||
|
val floatingBarBlurActive = asBundle.blur && asBundle.floatingBottomBarBlur
|
||||||
|
val surfaceColor = colorScheme.surface
|
||||||
|
val glassBackdrop = rememberLayerBackdrop {
|
||||||
|
drawRect(surfaceColor)
|
||||||
|
drawContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateToTab(tab: MainBottomTabDestination) {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(
|
||||||
|
page = tab.ordinal,
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
|
||||||
|
stiffness = UiMotion.PAGE_SWITCH_STIFFNESS,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
if (!asBundle.floatingBottomBar) {
|
||||||
tabs.forEach { tab ->
|
BlurredBar(backdrop = blurBackdrop) {
|
||||||
NavigationBarItem(
|
NavigationBar(
|
||||||
selected = currentTab == tab,
|
color =
|
||||||
onClick = {
|
if (blurBackdrop != null) Color.Transparent
|
||||||
scope.launch {
|
else colorScheme.surface,
|
||||||
pagerState.animateScrollToPage(
|
) {
|
||||||
page = tab.ordinal,
|
tabs.forEach { tab ->
|
||||||
animationSpec = spring(
|
NavigationBarItem(
|
||||||
dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
|
selected = currentTab == tab,
|
||||||
stiffness = UiMotion.PAGE_SWITCH_STIFFNESS,
|
onClick = { navigateToTab(tab) },
|
||||||
),
|
icon = tab.icon,
|
||||||
)
|
label = tab.label,
|
||||||
}
|
)
|
||||||
},
|
}
|
||||||
icon = tab.icon,
|
}
|
||||||
label = tab.label,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(snackHostState) },
|
snackbarHost = { SnackbarHost(snackHostState) },
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
HorizontalPager(
|
val bottomInnerPadding: Dp = if (asBundle.floatingBottomBar) {
|
||||||
modifier = Modifier
|
12.dp + 64.dp + contentPadding.calculateBottomPadding()
|
||||||
.fillMaxSize()
|
} else {
|
||||||
.padding(bottom = contentPadding.calculateBottomPadding()),
|
contentPadding.calculateBottomPadding()
|
||||||
state = pagerState,
|
|
||||||
beyondViewportPageCount = 1,
|
|
||||||
) { page ->
|
|
||||||
val tab = tabs[page]
|
|
||||||
saveableStateHolder.SaveableStateProvider(tab.name) {
|
|
||||||
when (tab) {
|
|
||||||
MainBottomTabDestination.Device -> DeviceTabScreen(
|
|
||||||
scrollBehavior = devicesPageScrollBehavior,
|
|
||||||
scrcpy = scrcpy,
|
|
||||||
onOpenReorderDevices = { showReorderDevices = true },
|
|
||||||
)
|
|
||||||
|
|
||||||
MainBottomTabDestination.Settings -> SettingsScreen(
|
|
||||||
scrollBehavior = settingsPageScrollBehavior,
|
|
||||||
onOpenReorderDevices = { showReorderDevices = true },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ReorderDevicesScreen(
|
Box(
|
||||||
show = showReorderDevices,
|
modifier = Modifier.fillMaxSize(),
|
||||||
onDismissRequest = { showReorderDevices = false },
|
) {
|
||||||
)
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.then(if (blurBackdrop != null) Modifier.miuixLayerBackdrop(blurBackdrop) else Modifier),
|
||||||
|
) {
|
||||||
|
HorizontalPager(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.then(
|
||||||
|
if (asBundle.floatingBottomBar && floatingBarBlurActive) {
|
||||||
|
Modifier.layerBackdrop(glassBackdrop)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
|
state = pagerState,
|
||||||
|
beyondViewportPageCount = 1,
|
||||||
|
) { page ->
|
||||||
|
val tab = tabs[page]
|
||||||
|
saveableStateHolder.SaveableStateProvider(tab.name) {
|
||||||
|
when (tab) {
|
||||||
|
MainBottomTabDestination.Device -> DeviceTabScreen(
|
||||||
|
scrollBehavior = devicesPageScrollBehavior,
|
||||||
|
scrcpy = scrcpy,
|
||||||
|
bottomInnerPadding = bottomInnerPadding,
|
||||||
|
onOpenReorderDevices = { showReorderDevices = true },
|
||||||
|
)
|
||||||
|
|
||||||
|
MainBottomTabDestination.Settings -> SettingsScreen(
|
||||||
|
scrollBehavior = settingsPageScrollBehavior,
|
||||||
|
bottomInnerPadding = bottomInnerPadding,
|
||||||
|
onOpenReorderDevices = { showReorderDevices = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asBundle.floatingBottomBar) {
|
||||||
|
FloatingBottomBar(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 12.dp + contentPadding.calculateBottomPadding())
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
selectedIndex = { pagerState.currentPage },
|
||||||
|
onSelected = { index ->
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(
|
||||||
|
page = index,
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = UiMotion.PAGE_SWITCH_DAMPING_RATIO,
|
||||||
|
stiffness = UiMotion.PAGE_SWITCH_STIFFNESS,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backdrop = glassBackdrop,
|
||||||
|
tabsCount = tabs.size,
|
||||||
|
isBlurEnabled = floatingBarBlurActive,
|
||||||
|
) {
|
||||||
|
tabs.forEach { tab ->
|
||||||
|
FloatingBottomBarItem(
|
||||||
|
onClick = { navigateToTab(tab) },
|
||||||
|
modifier = Modifier.defaultMinSize(minWidth = 76.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = tab.icon,
|
||||||
|
contentDescription = tab.label,
|
||||||
|
tint = colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = tab.label,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 14.sp,
|
||||||
|
color = colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false,
|
||||||
|
overflow = TextOverflow.Visible,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReorderDevicesScreen(
|
||||||
|
show = showReorderDevices,
|
||||||
|
onDismissRequest = { showReorderDevices = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,8 +518,14 @@ fun MainScreen() {
|
|||||||
}
|
}
|
||||||
val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
|
val themeController = remember(themeMode) { ThemeController(colorSchemeMode = themeMode) }
|
||||||
|
|
||||||
MiuixTheme(controller = themeController) {
|
MiuixTheme(
|
||||||
|
controller = themeController,
|
||||||
|
smoothRounding = asBundle.smoothCorner,
|
||||||
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
|
LocalEnableBlur provides asBundle.blur,
|
||||||
|
LocalEnableFloatingBottomBar provides asBundle.floatingBottomBar,
|
||||||
|
LocalEnableFloatingBottomBarBlur provides asBundle.floatingBottomBarBlur,
|
||||||
LocalRootNavigator provides rootNavigator,
|
LocalRootNavigator provides rootNavigator,
|
||||||
LocalSnackbarController provides snackbarController,
|
LocalSnackbarController provides snackbarController,
|
||||||
LocalAppHaptics provides haptics,
|
LocalAppHaptics provides haptics,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package io.github.miuzarte.scrcpyforandroid.pages
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -34,6 +35,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusDirection
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -63,6 +65,9 @@ import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
|||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.quickDevices
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyProfiles
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyProfiles
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList
|
import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -87,6 +92,7 @@ import top.yukonga.miuix.kmp.basic.Text
|
|||||||
import top.yukonga.miuix.kmp.basic.TextButton
|
import top.yukonga.miuix.kmp.basic.TextButton
|
||||||
import top.yukonga.miuix.kmp.basic.TextField
|
import top.yukonga.miuix.kmp.basic.TextField
|
||||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||||
|
import top.yukonga.miuix.kmp.blur.layerBackdrop
|
||||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||||
import top.yukonga.miuix.kmp.icon.extended.Store
|
import top.yukonga.miuix.kmp.icon.extended.Store
|
||||||
import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet
|
import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet
|
||||||
@@ -96,7 +102,7 @@ import top.yukonga.miuix.kmp.preference.ArrowPreference
|
|||||||
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
||||||
import top.yukonga.miuix.kmp.preference.OverlaySpinnerPreference
|
import top.yukonga.miuix.kmp.preference.OverlaySpinnerPreference
|
||||||
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -105,6 +111,8 @@ internal fun ScrcpyAllOptionsScreen(
|
|||||||
scrcpy: Scrcpy,
|
scrcpy: Scrcpy,
|
||||||
) {
|
) {
|
||||||
val navigator = LocalRootNavigator.current
|
val navigator = LocalRootNavigator.current
|
||||||
|
val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current)
|
||||||
|
val blurActive = blurBackdrop != null
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val snackbar = LocalSnackbarController.current
|
val snackbar = LocalSnackbarController.current
|
||||||
var showProfileMenu by rememberSaveable { mutableStateOf(false) }
|
var showProfileMenu by rememberSaveable { mutableStateOf(false) }
|
||||||
@@ -211,81 +219,92 @@ internal fun ScrcpyAllOptionsScreen(
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
BlurredBar(backdrop = blurBackdrop) {
|
||||||
title = "所有参数",
|
TopAppBar(
|
||||||
navigationIcon = {
|
title = "所有参数",
|
||||||
IconButton(onClick = navigator.pop) {
|
color =
|
||||||
Icon(
|
if (blurActive) Color.Transparent
|
||||||
Icons.AutoMirrored.Rounded.ArrowBack,
|
else colorScheme.surface,
|
||||||
contentDescription = "返回"
|
navigationIcon = {
|
||||||
|
IconButton(onClick = navigator.pop) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Rounded.ArrowBack,
|
||||||
|
contentDescription = "返回"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { showProfileMenu = true },
|
||||||
|
holdDownState = showProfileMenu,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.MoreVert,
|
||||||
|
contentDescription = "配置管理",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ProfileMenuPopup(
|
||||||
|
show = showProfileMenu,
|
||||||
|
onDismissRequest = { showProfileMenu = false },
|
||||||
|
onManageProfiles = {
|
||||||
|
showManageProfilesSheet = true
|
||||||
|
showProfileMenu = false
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
},
|
scrollBehavior = scrollBehavior,
|
||||||
actions = {
|
bottomContent = {
|
||||||
IconButton(
|
TabRow(
|
||||||
onClick = { showProfileMenu = true },
|
tabs = profileTabs,
|
||||||
holdDownState = showProfileMenu,
|
selectedTabIndex = selectedProfileIndex,
|
||||||
) {
|
onTabSelected = { index ->
|
||||||
Icon(
|
val nextProfileId = profileIds.getOrNull(index)
|
||||||
Icons.Rounded.MoreVert,
|
?: return@TabRow
|
||||||
contentDescription = "配置管理",
|
if (nextProfileId == selectedProfileId) return@TabRow
|
||||||
)
|
scope.launch {
|
||||||
}
|
saveBundleForProfile(selectedProfileId, soBundleState.value)
|
||||||
ProfileMenuPopup(
|
bindCurrentConnectedDevice(nextProfileId)
|
||||||
show = showProfileMenu,
|
selectedProfileId = nextProfileId
|
||||||
onDismissRequest = { showProfileMenu = false },
|
val profileName = profileTabs.getOrElse(index) { "全局" }
|
||||||
onManageProfiles = {
|
currentConnectedDeviceName?.let { deviceName ->
|
||||||
showManageProfilesSheet = true
|
snackbar.show("$deviceName 已切换到配置 $profileName")
|
||||||
showProfileMenu = false
|
}
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
bottomContent = {
|
|
||||||
TabRow(
|
|
||||||
tabs = profileTabs,
|
|
||||||
selectedTabIndex = selectedProfileIndex,
|
|
||||||
onTabSelected = { index ->
|
|
||||||
val nextProfileId = profileIds.getOrNull(index)
|
|
||||||
?: return@TabRow
|
|
||||||
if (nextProfileId == selectedProfileId) return@TabRow
|
|
||||||
scope.launch {
|
|
||||||
saveBundleForProfile(selectedProfileId, soBundleState.value)
|
|
||||||
bindCurrentConnectedDevice(nextProfileId)
|
|
||||||
selectedProfileId = nextProfileId
|
|
||||||
val profileName = profileTabs.getOrElse(index) { "全局" }
|
|
||||||
currentConnectedDeviceName?.let { deviceName ->
|
|
||||||
snackbar.show("$deviceName 已切换到配置 $profileName")
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.padding(bottom = UiSpacing.Medium)
|
||||||
.padding(bottom = UiSpacing.Medium)
|
.padding(horizontal = UiSpacing.Medium),
|
||||||
.padding(horizontal = UiSpacing.Medium),
|
minWidth = 96.dp,
|
||||||
minWidth = 96.dp,
|
maxWidth = 192.dp,
|
||||||
maxWidth = 192.dp,
|
height = 48.dp,
|
||||||
height = 48.dp,
|
itemSpacing = UiSpacing.Medium,
|
||||||
itemSpacing = UiSpacing.Medium,
|
)
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
},
|
},
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
val snackbar = LocalSnackbarController.current
|
val snackbar = LocalSnackbarController.current
|
||||||
SnackbarHost(snackbar.hostState)
|
SnackbarHost(snackbar.hostState)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
ScrcpyAllOptionsPage(
|
Box(
|
||||||
contentPadding = contentPadding,
|
modifier =
|
||||||
scrollBehavior = scrollBehavior,
|
if (blurActive) Modifier.layerBackdrop(blurBackdrop)
|
||||||
scrcpy = scrcpy,
|
else Modifier,
|
||||||
soBundleShared = soBundleShared,
|
) {
|
||||||
scrcpyProfilesState = scrcpyProfilesState,
|
ScrcpyAllOptionsPage(
|
||||||
selectedProfileIdState = selectedProfileIdState,
|
contentPadding = contentPadding,
|
||||||
soBundleState = soBundleState,
|
scrollBehavior = scrollBehavior,
|
||||||
lastValidSoBundleState = lastValidSoBundleState,
|
scrcpy = scrcpy,
|
||||||
onSaveBundleForProfile = ::saveBundleForProfile,
|
soBundleShared = soBundleShared,
|
||||||
)
|
scrcpyProfilesState = scrcpyProfilesState,
|
||||||
|
selectedProfileIdState = selectedProfileIdState,
|
||||||
|
soBundleState = soBundleState,
|
||||||
|
lastValidSoBundleState = lastValidSoBundleState,
|
||||||
|
onSaveBundleForProfile = ::saveBundleForProfile,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ProfileNameDialog(
|
ProfileNameDialog(
|
||||||
mode = activeProfileDialog,
|
mode = activeProfileDialog,
|
||||||
@@ -752,6 +771,7 @@ internal fun ScrcpyAllOptionsPage(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
bottomInnerPadding = UiSpacing.PageBottom,
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Card {
|
Card {
|
||||||
@@ -1854,7 +1874,6 @@ internal fun ScrcpyAllOptionsPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item { Spacer(Modifier.height(UiSpacing.PageBottom)) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1874,7 +1893,6 @@ private fun ProfileMenuPopup(
|
|||||||
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
|
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
|
||||||
alignment = PopupPositionProvider.Align.TopEnd,
|
alignment = PopupPositionProvider.Align.TopEnd,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
enableWindowDim = false,
|
|
||||||
) {
|
) {
|
||||||
ListPopupColumn {
|
ListPopupColumn {
|
||||||
ProfileMenuPopupItem(
|
ProfileMenuPopupItem(
|
||||||
@@ -1915,7 +1933,7 @@ private fun ProfileMenuPopupItem(
|
|||||||
top = if (index == 0) UiSpacing.PopupHorizontal else UiSpacing.PageItem,
|
top = if (index == 0) UiSpacing.PopupHorizontal else UiSpacing.PageItem,
|
||||||
bottom = if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem,
|
bottom = if (index == optionSize - 1) UiSpacing.PopupHorizontal else UiSpacing.PageItem,
|
||||||
),
|
),
|
||||||
color = MiuixTheme.colorScheme.disabledOnSecondaryVariant,
|
color = colorScheme.disabledOnSecondaryVariant,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import android.content.Intent
|
|||||||
import android.os.Process
|
import android.os.Process
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Clear
|
import androidx.compose.material.icons.rounded.Clear
|
||||||
@@ -26,8 +25,10 @@ import androidx.compose.runtime.rememberUpdatedState
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import io.github.miuzarte.scrcpyforandroid.BuildConfig
|
import io.github.miuzarte.scrcpyforandroid.BuildConfig
|
||||||
import io.github.miuzarte.scrcpyforandroid.LockscreenPasswordActivity
|
import io.github.miuzarte.scrcpyforandroid.LockscreenPasswordActivity
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
|
import io.github.miuzarte.scrcpyforandroid.constants.ThemeModes
|
||||||
@@ -42,6 +43,9 @@ import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
|||||||
import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration
|
import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle
|
import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -56,30 +60,48 @@ import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
|||||||
import top.yukonga.miuix.kmp.basic.Text
|
import top.yukonga.miuix.kmp.basic.Text
|
||||||
import top.yukonga.miuix.kmp.basic.TextField
|
import top.yukonga.miuix.kmp.basic.TextField
|
||||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||||
|
import top.yukonga.miuix.kmp.blur.layerBackdrop
|
||||||
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
||||||
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
||||||
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
scrollBehavior: ScrollBehavior,
|
scrollBehavior: ScrollBehavior,
|
||||||
|
bottomInnerPadding: Dp,
|
||||||
onOpenReorderDevices: () -> Unit,
|
onOpenReorderDevices: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current)
|
||||||
|
val blurActive = blurBackdrop != null
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
BlurredBar(backdrop = blurBackdrop) {
|
||||||
title = "设置",
|
TopAppBar(
|
||||||
scrollBehavior = scrollBehavior,
|
title = "设置",
|
||||||
)
|
color =
|
||||||
|
if (blurActive) Color.Transparent
|
||||||
|
else colorScheme.surface,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) { pagePadding ->
|
) { pagePadding ->
|
||||||
SettingsPage(
|
Box(
|
||||||
contentPadding = pagePadding,
|
modifier =
|
||||||
scrollBehavior = scrollBehavior,
|
if (blurActive) Modifier.layerBackdrop(blurBackdrop)
|
||||||
onOpenReorderDevices = onOpenReorderDevices,
|
else Modifier,
|
||||||
)
|
) {
|
||||||
|
SettingsPage(
|
||||||
|
contentPadding = pagePadding,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
bottomInnerPadding = bottomInnerPadding,
|
||||||
|
onOpenReorderDevices = onOpenReorderDevices,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +109,7 @@ fun SettingsScreen(
|
|||||||
fun SettingsPage(
|
fun SettingsPage(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
scrollBehavior: ScrollBehavior,
|
scrollBehavior: ScrollBehavior,
|
||||||
|
bottomInnerPadding: Dp,
|
||||||
onOpenReorderDevices: () -> Unit,
|
onOpenReorderDevices: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -172,6 +195,7 @@ fun SettingsPage(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
bottomInnerPadding = bottomInnerPadding,
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
SectionSmallTitle("主题")
|
SectionSmallTitle("主题")
|
||||||
@@ -180,30 +204,60 @@ fun SettingsPage(
|
|||||||
title = "外观模式",
|
title = "外观模式",
|
||||||
summary = ThemeModes.baseOptions
|
summary = ThemeModes.baseOptions
|
||||||
.getOrNull(
|
.getOrNull(
|
||||||
asBundle.themeBaseIndex.coerceIn(
|
asBundle.themeBaseIndex
|
||||||
0,
|
.coerceIn(0, ThemeModes.baseOptions.lastIndex)
|
||||||
ThemeModes.baseOptions.lastIndex
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
?.label
|
?.label
|
||||||
?: "跟随系统",
|
?: "跟随系统",
|
||||||
items = themeItems,
|
items = themeItems,
|
||||||
selectedIndex = asBundle.themeBaseIndex.coerceIn(
|
selectedIndex = asBundle.themeBaseIndex
|
||||||
0,
|
.coerceIn(0, ThemeModes.baseOptions.lastIndex),
|
||||||
ThemeModes.baseOptions.lastIndex
|
|
||||||
),
|
|
||||||
onSelectedIndexChange = {
|
onSelectedIndexChange = {
|
||||||
asBundle = asBundle.copy(themeBaseIndex = it)
|
asBundle = asBundle.copy(themeBaseIndex = it)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
title = "Monet",
|
title = "Monet 颜色",
|
||||||
summary = "开启后使用 Monet 动态配色",
|
summary = "开启后使用 Monet 动态配色",
|
||||||
checked = asBundle.monet,
|
checked = asBundle.monet,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
asBundle = asBundle.copy(monet = it)
|
asBundle = asBundle.copy(monet = it)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
SwitchPreference(
|
||||||
|
title = "模糊",
|
||||||
|
summary = "启用顶栏和底栏的模糊效果",
|
||||||
|
checked = asBundle.blur,
|
||||||
|
onCheckedChange = {
|
||||||
|
asBundle = asBundle.copy(blur = it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SwitchPreference(
|
||||||
|
title = "悬浮底栏",
|
||||||
|
summary = "使用 Apple 风格的悬浮底栏",
|
||||||
|
checked = asBundle.floatingBottomBar,
|
||||||
|
onCheckedChange = {
|
||||||
|
asBundle = asBundle.copy(floatingBottomBar = it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
AnimatedVisibility(asBundle.floatingBottomBar) {
|
||||||
|
SwitchPreference(
|
||||||
|
title = "液态玻璃",
|
||||||
|
summary = "启用悬浮底栏的液态玻璃效果",
|
||||||
|
checked = asBundle.floatingBottomBarBlur,
|
||||||
|
onCheckedChange = {
|
||||||
|
asBundle = asBundle.copy(floatingBottomBarBlur = it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SwitchPreference(
|
||||||
|
title = "平滑圆角",
|
||||||
|
summary = "启用全局平滑圆角效果",
|
||||||
|
checked = asBundle.smoothCorner,
|
||||||
|
onCheckedChange = {
|
||||||
|
asBundle = asBundle.copy(smoothCorner = it)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,6 +554,5 @@ fun SettingsPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item { Spacer(Modifier.height(UiSpacing.PageBottom)) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,10 @@ fun StreamScreen(activity: StreamActivity) {
|
|||||||
SnackbarController(scope = snackbarScope, hostState = snackbarHostState)
|
SnackbarController(scope = snackbarScope, hostState = snackbarHostState)
|
||||||
}
|
}
|
||||||
|
|
||||||
MiuixTheme(controller = themeController) {
|
MiuixTheme(
|
||||||
|
controller = themeController,
|
||||||
|
smoothRounding = asBundle.smoothCorner,
|
||||||
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalSnackbarController provides snackbarController,
|
LocalSnackbarController provides snackbarController,
|
||||||
LocalAppHaptics provides haptics,
|
LocalAppHaptics provides haptics,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.pages
|
package io.github.miuzarte.scrcpyforandroid.pages
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -12,16 +13,19 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
|
import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList
|
import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
|
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
|
||||||
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
|
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
|
||||||
@@ -36,34 +40,45 @@ import top.yukonga.miuix.kmp.basic.IconButton
|
|||||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||||
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
import top.yukonga.miuix.kmp.basic.ScrollBehavior
|
||||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||||
|
import top.yukonga.miuix.kmp.blur.layerBackdrop
|
||||||
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun VirtualButtonOrderScreen(
|
internal fun VirtualButtonOrderScreen(
|
||||||
scrollBehavior: ScrollBehavior,
|
scrollBehavior: ScrollBehavior,
|
||||||
) {
|
) {
|
||||||
val navigator = LocalRootNavigator.current
|
val navigator = LocalRootNavigator.current
|
||||||
|
val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current)
|
||||||
|
val blurActive = blurBackdrop != null
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
BlurredBar(backdrop = blurBackdrop) {
|
||||||
title = "虚拟按钮排序",
|
TopAppBar(
|
||||||
navigationIcon = {
|
title = "虚拟按钮排序",
|
||||||
IconButton(onClick = navigator.pop) {
|
color =
|
||||||
Icon(
|
if (blurActive) Color.Transparent
|
||||||
Icons.AutoMirrored.Rounded.ArrowBack,
|
else colorScheme.surface,
|
||||||
contentDescription = "返回"
|
navigationIcon = {
|
||||||
)
|
IconButton(onClick = navigator.pop) {
|
||||||
}
|
Icon(
|
||||||
},
|
Icons.AutoMirrored.Rounded.ArrowBack,
|
||||||
scrollBehavior = scrollBehavior,
|
contentDescription = "返回"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) { pagePadding ->
|
) { pagePadding ->
|
||||||
VirtualButtonOrderPage(
|
Box(modifier = if (blurActive) Modifier.layerBackdrop(blurBackdrop) else Modifier) {
|
||||||
contentPadding = pagePadding,
|
VirtualButtonOrderPage(
|
||||||
scrollBehavior = scrollBehavior,
|
contentPadding = pagePadding,
|
||||||
)
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +87,6 @@ internal fun VirtualButtonOrderPage(
|
|||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
scrollBehavior: ScrollBehavior,
|
scrollBehavior: ScrollBehavior,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||||
|
|
||||||
val asBundleShared by appSettings.bundleState.collectAsState()
|
val asBundleShared by appSettings.bundleState.collectAsState()
|
||||||
@@ -105,8 +119,8 @@ internal fun VirtualButtonOrderPage(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
bottomInnerPadding = UiSpacing.PageBottom,
|
||||||
) {
|
) {
|
||||||
// 按钮显示文本开关
|
|
||||||
item {
|
item {
|
||||||
Card {
|
Card {
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
@@ -130,8 +144,12 @@ internal fun VirtualButtonOrderPage(
|
|||||||
ReorderableList.Item(
|
ReorderableList.Item(
|
||||||
id = action.id,
|
id = action.id,
|
||||||
icon = action.icon,
|
icon = action.icon,
|
||||||
title = if (action.keycode == null) action.title else "${action.title} (${action.keycode})",
|
title =
|
||||||
subtitle = if (item.showOutside) "显示在外部" else "显示在更多菜单内",
|
if (action.keycode == null) action.title
|
||||||
|
else "${action.title} (${action.keycode})",
|
||||||
|
subtitle =
|
||||||
|
if (item.showOutside) "显示在外部"
|
||||||
|
else "显示在更多菜单内",
|
||||||
endActions = listOf(
|
endActions = listOf(
|
||||||
ReorderableList.EndAction.Checkbox(
|
ReorderableList.EndAction.Checkbox(
|
||||||
checked = item.showOutside,
|
checked = item.showOutside,
|
||||||
@@ -139,16 +157,13 @@ internal fun VirtualButtonOrderPage(
|
|||||||
onClick = {
|
onClick = {
|
||||||
val checked = !item.showOutside
|
val checked = !item.showOutside
|
||||||
buttonItems = buttonItems.map { current ->
|
buttonItems = buttonItems.map { current ->
|
||||||
if (current.action.id == action.id) {
|
if (current.action.id == action.id)
|
||||||
current.copy(showOutside = checked)
|
current.copy(showOutside = checked)
|
||||||
} else {
|
else current
|
||||||
current
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
asBundle = asBundle.copy(
|
asBundle = asBundle.copy(
|
||||||
virtualButtonsLayout = VirtualButtonActions.encodeStoredLayout(
|
virtualButtonsLayout = VirtualButtonActions
|
||||||
buttonItems
|
.encodeStoredLayout(buttonItems)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -158,11 +173,11 @@ internal fun VirtualButtonOrderPage(
|
|||||||
},
|
},
|
||||||
orientation = ReorderableList.Orientation.Column,
|
orientation = ReorderableList.Orientation.Column,
|
||||||
onSettle = { fromIndex, toIndex ->
|
onSettle = { fromIndex, toIndex ->
|
||||||
buttonItems = buttonItems.toMutableList().apply {
|
buttonItems = buttonItems.toMutableList()
|
||||||
add(toIndex, removeAt(fromIndex))
|
.apply { add(toIndex, removeAt(fromIndex)) }
|
||||||
}
|
|
||||||
asBundle = asBundle.copy(
|
asBundle = asBundle.copy(
|
||||||
virtualButtonsLayout = VirtualButtonActions.encodeStoredLayout(buttonItems)
|
virtualButtonsLayout = VirtualButtonActions
|
||||||
|
.encodeStoredLayout(buttonItems)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)()
|
)()
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.pages.effect
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BgEffectBackground(
|
||||||
|
dynamicBackground: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
bgModifier: Modifier = Modifier,
|
||||||
|
isFullSize: Boolean = false,
|
||||||
|
effectBackground: Boolean = true,
|
||||||
|
isOs3Effect: Boolean = true,
|
||||||
|
alpha: () -> Float = { 1f },
|
||||||
|
content: @Composable (BoxScope.() -> Unit),
|
||||||
|
) {
|
||||||
|
val shaderSupported = remember { isRuntimeShaderSupported() }
|
||||||
|
if (!shaderSupported) {
|
||||||
|
Box(modifier = modifier, content = content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
val surface = colorScheme.surface
|
||||||
|
val painter = remember(isOs3Effect) { BgEffectPainter(isOs3Effect) }
|
||||||
|
val animTime = rememberFrameTimeSeconds(dynamicBackground)
|
||||||
|
val isDark = colorScheme.background.luminance() < 0.5f
|
||||||
|
val deviceType = DeviceType.PHONE
|
||||||
|
|
||||||
|
val preset = remember(isDark, deviceType, isOs3Effect) {
|
||||||
|
BgEffectConfig.get(deviceType, isDark, isOs3Effect)
|
||||||
|
}
|
||||||
|
|
||||||
|
val colorStage = remember { Animatable(0f) }
|
||||||
|
|
||||||
|
LaunchedEffect(dynamicBackground, preset) {
|
||||||
|
if (!dynamicBackground) return@LaunchedEffect
|
||||||
|
var targetStage = 1f
|
||||||
|
while (isActive) {
|
||||||
|
delay((preset.colorInterpPeriod * 500).toLong())
|
||||||
|
colorStage.animateTo(
|
||||||
|
targetValue = targetStage,
|
||||||
|
animationSpec = spring(dampingRatio = 0.9f, stiffness = 35f),
|
||||||
|
)
|
||||||
|
targetStage += 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key(isOs3Effect) {
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.then(bgModifier),
|
||||||
|
) {
|
||||||
|
drawRect(surface)
|
||||||
|
if (effectBackground) {
|
||||||
|
val drawHeight = if (isFullSize) size.height else size.height * 0.78f
|
||||||
|
|
||||||
|
val stage = colorStage.value
|
||||||
|
val base = stage.toInt()
|
||||||
|
val fraction = stage - base
|
||||||
|
|
||||||
|
val getColors = { index: Int ->
|
||||||
|
when (index % 4) {
|
||||||
|
0 -> preset.colors2
|
||||||
|
1 -> preset.colors1
|
||||||
|
2 -> preset.colors2
|
||||||
|
3 -> preset.colors3
|
||||||
|
else -> preset.colors2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val start = getColors(base)
|
||||||
|
val end = getColors(base + 1)
|
||||||
|
val currentColors = FloatArray(16) { i ->
|
||||||
|
start[i] + (end[i] - start[i]) * fraction
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.updateResolution(size.width, size.height)
|
||||||
|
painter.updatePresetIfNeeded(drawHeight, size.height, size.width, isDark)
|
||||||
|
painter.updateColors(currentColors)
|
||||||
|
painter.updateAnimTime(animTime())
|
||||||
|
|
||||||
|
drawRect(painter.brush, alpha = alpha())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.pages.effect
|
||||||
|
|
||||||
|
internal enum class DeviceType {
|
||||||
|
PHONE,
|
||||||
|
PAD,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object BgEffectConfig {
|
||||||
|
|
||||||
|
internal class Config(
|
||||||
|
val points: FloatArray,
|
||||||
|
val colors1: FloatArray,
|
||||||
|
val colors2: FloatArray,
|
||||||
|
val colors3: FloatArray,
|
||||||
|
val colorInterpPeriod: Float,
|
||||||
|
val lightOffset: Float,
|
||||||
|
val saturateOffset: Float,
|
||||||
|
val pointOffset: Float,
|
||||||
|
// OS3 specific
|
||||||
|
val shadowColorMulti: Float = 0.3f,
|
||||||
|
val shadowColorOffset: Float = 0.3f,
|
||||||
|
val shadowNoiseScale: Float = 5.0f,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun get(
|
||||||
|
deviceType: DeviceType,
|
||||||
|
isDark: Boolean,
|
||||||
|
isOs3: Boolean,
|
||||||
|
): Config {
|
||||||
|
return if (isOs3) {
|
||||||
|
getOs3Config(deviceType, isDark)
|
||||||
|
} else {
|
||||||
|
getOs2Config(deviceType, isDark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.pages.effect
|
||||||
|
|
||||||
|
private val OS2_PHONE_LIGHT_COLORS = floatArrayOf(
|
||||||
|
0.57f, 0.76f, 0.98f, 1.0f,
|
||||||
|
0.98f, 0.85f, 0.68f, 1.0f,
|
||||||
|
0.98f, 0.75f, 0.93f, 1.0f,
|
||||||
|
0.73f, 0.70f, 0.98f, 1.0f,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val OS2_PHONE_LIGHT = BgEffectConfig.Config(
|
||||||
|
points = floatArrayOf(
|
||||||
|
0.67f,
|
||||||
|
0.42f,
|
||||||
|
1.0f,
|
||||||
|
0.69f,
|
||||||
|
0.75f,
|
||||||
|
1.0f,
|
||||||
|
0.14f,
|
||||||
|
0.71f,
|
||||||
|
0.95f,
|
||||||
|
0.14f,
|
||||||
|
0.27f,
|
||||||
|
0.8f
|
||||||
|
),
|
||||||
|
colors1 = OS2_PHONE_LIGHT_COLORS,
|
||||||
|
colors2 = OS2_PHONE_LIGHT_COLORS,
|
||||||
|
colors3 = OS2_PHONE_LIGHT_COLORS,
|
||||||
|
colorInterpPeriod = 100f,
|
||||||
|
lightOffset = 0.1f,
|
||||||
|
saturateOffset = 0.2f,
|
||||||
|
pointOffset = 0.1f,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val OS2_PHONE_DARK_COLORS = floatArrayOf(
|
||||||
|
0.0f, 0.31f, 0.58f, 1.0f,
|
||||||
|
0.53f, 0.29f, 0.15f, 1.0f,
|
||||||
|
0.46f, 0.06f, 0.27f, 1.0f,
|
||||||
|
0.16f, 0.12f, 0.45f, 1.0f,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val OS2_PHONE_DARK = BgEffectConfig.Config(
|
||||||
|
points = floatArrayOf(
|
||||||
|
0.63f,
|
||||||
|
0.50f,
|
||||||
|
0.88f,
|
||||||
|
0.69f,
|
||||||
|
0.75f,
|
||||||
|
0.80f,
|
||||||
|
0.17f,
|
||||||
|
0.66f,
|
||||||
|
0.81f,
|
||||||
|
0.14f,
|
||||||
|
0.24f,
|
||||||
|
0.72f
|
||||||
|
),
|
||||||
|
colors1 = OS2_PHONE_DARK_COLORS,
|
||||||
|
colors2 = OS2_PHONE_DARK_COLORS,
|
||||||
|
colors3 = OS2_PHONE_DARK_COLORS,
|
||||||
|
colorInterpPeriod = 100f,
|
||||||
|
lightOffset = -0.1f,
|
||||||
|
saturateOffset = 0.2f,
|
||||||
|
pointOffset = 0.1f,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val OS2_PAD_LIGHT_COLORS = floatArrayOf(
|
||||||
|
0.57f, 0.76f, 0.98f, 1.0f,
|
||||||
|
0.98f, 0.85f, 0.68f, 1.0f,
|
||||||
|
0.98f, 0.75f, 0.93f, 0.95f,
|
||||||
|
0.73f, 0.70f, 0.98f, 0.90f,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val OS2_PAD_LIGHT = BgEffectConfig.Config(
|
||||||
|
points = floatArrayOf(
|
||||||
|
0.67f,
|
||||||
|
0.37f,
|
||||||
|
0.88f,
|
||||||
|
0.54f,
|
||||||
|
0.66f,
|
||||||
|
1.0f,
|
||||||
|
0.37f,
|
||||||
|
0.71f,
|
||||||
|
0.68f,
|
||||||
|
0.28f,
|
||||||
|
0.26f,
|
||||||
|
0.62f
|
||||||
|
),
|
||||||
|
colors1 = OS2_PAD_LIGHT_COLORS,
|
||||||
|
colors2 = OS2_PAD_LIGHT_COLORS,
|
||||||
|
colors3 = OS2_PAD_LIGHT_COLORS,
|
||||||
|
colorInterpPeriod = 100f,
|
||||||
|
lightOffset = 0.1f,
|
||||||
|
saturateOffset = 0f,
|
||||||
|
pointOffset = 0.1f,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val OS2_PAD_DARK_COLORS = floatArrayOf(
|
||||||
|
0.0f, 0.31f, 0.58f, 1.0f,
|
||||||
|
0.53f, 0.29f, 0.15f, 1.0f,
|
||||||
|
0.46f, 0.06f, 0.27f, 1.0f,
|
||||||
|
0.16f, 0.12f, 0.45f, 1.0f,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val OS2_PAD_DARK = BgEffectConfig.Config(
|
||||||
|
points = floatArrayOf(
|
||||||
|
0.55f,
|
||||||
|
0.42f,
|
||||||
|
1.0f,
|
||||||
|
0.56f,
|
||||||
|
0.75f,
|
||||||
|
1.0f,
|
||||||
|
0.40f,
|
||||||
|
0.59f,
|
||||||
|
0.71f,
|
||||||
|
0.43f,
|
||||||
|
0.09f,
|
||||||
|
0.75f
|
||||||
|
),
|
||||||
|
colors1 = OS2_PAD_DARK_COLORS,
|
||||||
|
colors2 = OS2_PAD_DARK_COLORS,
|
||||||
|
colors3 = OS2_PAD_DARK_COLORS,
|
||||||
|
colorInterpPeriod = 100f,
|
||||||
|
lightOffset = -0.1f,
|
||||||
|
saturateOffset = 0.2f,
|
||||||
|
pointOffset = 0.1f,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun getOs2Config(deviceType: DeviceType, isDark: Boolean): BgEffectConfig.Config {
|
||||||
|
return when (deviceType) {
|
||||||
|
DeviceType.PHONE -> if (isDark) OS2_PHONE_DARK else OS2_PHONE_LIGHT
|
||||||
|
DeviceType.PAD -> if (isDark) OS2_PAD_DARK else OS2_PAD_LIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.pages.effect
|
||||||
|
|
||||||
|
private val OS3_PHONE_LIGHT = BgEffectConfig.Config(
|
||||||
|
points = floatArrayOf(0.8f, 0.2f, 1.0f, 0.8f, 0.9f, 1.0f, 0.2f, 0.9f, 1.0f, 0.2f, 0.2f, 1.0f),
|
||||||
|
colors1 = floatArrayOf(
|
||||||
|
1.0f,
|
||||||
|
0.9f,
|
||||||
|
0.94f,
|
||||||
|
1.0f,
|
||||||
|
1.0f,
|
||||||
|
0.84f,
|
||||||
|
0.89f,
|
||||||
|
1.0f,
|
||||||
|
0.97f,
|
||||||
|
0.73f,
|
||||||
|
0.82f,
|
||||||
|
1.0f,
|
||||||
|
0.64f,
|
||||||
|
0.65f,
|
||||||
|
0.98f,
|
||||||
|
1.0f
|
||||||
|
),
|
||||||
|
colors2 = floatArrayOf(
|
||||||
|
0.58f,
|
||||||
|
0.74f,
|
||||||
|
1.0f,
|
||||||
|
1.0f,
|
||||||
|
1.0f,
|
||||||
|
0.9f,
|
||||||
|
0.93f,
|
||||||
|
1.0f,
|
||||||
|
0.74f,
|
||||||
|
0.76f,
|
||||||
|
1.0f,
|
||||||
|
1.0f,
|
||||||
|
0.97f,
|
||||||
|
0.77f,
|
||||||
|
0.84f,
|
||||||
|
1.0f
|
||||||
|
),
|
||||||
|
colors3 = floatArrayOf(
|
||||||
|
0.98f,
|
||||||
|
0.86f,
|
||||||
|
0.9f,
|
||||||
|
1.0f,
|
||||||
|
0.6f,
|
||||||
|
0.73f,
|
||||||
|
0.98f,
|
||||||
|
1.0f,
|
||||||
|
0.92f,
|
||||||
|
0.93f,
|
||||||
|
1.0f,
|
||||||
|
1.0f,
|
||||||
|
0.56f,
|
||||||
|
0.69f,
|
||||||
|
1.0f,
|
||||||
|
1.0f
|
||||||
|
),
|
||||||
|
colorInterpPeriod = 5.0f,
|
||||||
|
lightOffset = 0.1f,
|
||||||
|
saturateOffset = 0.2f,
|
||||||
|
pointOffset = 0.2f,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val OS3_PHONE_DARK = BgEffectConfig.Config(
|
||||||
|
points = floatArrayOf(0.8f, 0.2f, 1.0f, 0.8f, 0.9f, 1.0f, 0.2f, 0.9f, 1.0f, 0.2f, 0.2f, 1.0f),
|
||||||
|
colors1 = floatArrayOf(
|
||||||
|
0.2f,
|
||||||
|
0.06f,
|
||||||
|
0.88f,
|
||||||
|
0.4f,
|
||||||
|
0.3f,
|
||||||
|
0.14f,
|
||||||
|
0.55f,
|
||||||
|
0.5f,
|
||||||
|
0.0f,
|
||||||
|
0.64f,
|
||||||
|
0.96f,
|
||||||
|
0.5f,
|
||||||
|
0.11f,
|
||||||
|
0.16f,
|
||||||
|
0.83f,
|
||||||
|
0.4f
|
||||||
|
),
|
||||||
|
colors2 = floatArrayOf(
|
||||||
|
0.07f,
|
||||||
|
0.15f,
|
||||||
|
0.79f,
|
||||||
|
0.5f,
|
||||||
|
0.62f,
|
||||||
|
0.21f,
|
||||||
|
0.67f,
|
||||||
|
0.5f,
|
||||||
|
0.06f,
|
||||||
|
0.25f,
|
||||||
|
0.84f,
|
||||||
|
0.5f,
|
||||||
|
0.0f,
|
||||||
|
0.2f,
|
||||||
|
0.78f,
|
||||||
|
0.5f
|
||||||
|
),
|
||||||
|
colors3 = floatArrayOf(
|
||||||
|
0.58f,
|
||||||
|
0.3f,
|
||||||
|
0.74f,
|
||||||
|
0.4f,
|
||||||
|
0.27f,
|
||||||
|
0.18f,
|
||||||
|
0.6f,
|
||||||
|
0.5f,
|
||||||
|
0.66f,
|
||||||
|
0.26f,
|
||||||
|
0.62f,
|
||||||
|
0.5f,
|
||||||
|
0.12f,
|
||||||
|
0.16f,
|
||||||
|
0.7f,
|
||||||
|
0.6f
|
||||||
|
),
|
||||||
|
colorInterpPeriod = 8.0f,
|
||||||
|
lightOffset = 0.0f,
|
||||||
|
saturateOffset = 0.17f,
|
||||||
|
pointOffset = 0.4f,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val OS3_PAD_LIGHT = BgEffectConfig.Config(
|
||||||
|
points = floatArrayOf(0.8f, 0.2f, 1.0f, 0.8f, 0.9f, 1.0f, 0.2f, 0.9f, 1.0f, 0.2f, 0.2f, 1.0f),
|
||||||
|
colors1 = floatArrayOf(
|
||||||
|
0.99f,
|
||||||
|
0.77f,
|
||||||
|
0.86f,
|
||||||
|
1.0f,
|
||||||
|
0.74f,
|
||||||
|
0.76f,
|
||||||
|
1.0f,
|
||||||
|
1.0f,
|
||||||
|
0.72f,
|
||||||
|
0.74f,
|
||||||
|
1.0f,
|
||||||
|
1.0f,
|
||||||
|
0.98f,
|
||||||
|
0.76f,
|
||||||
|
0.8f,
|
||||||
|
1.0f
|
||||||
|
),
|
||||||
|
colors2 = floatArrayOf(
|
||||||
|
0.66f,
|
||||||
|
0.75f,
|
||||||
|
1.0f,
|
||||||
|
1.0f,
|
||||||
|
1.0f,
|
||||||
|
0.86f,
|
||||||
|
0.91f,
|
||||||
|
1.0f,
|
||||||
|
0.74f,
|
||||||
|
0.76f,
|
||||||
|
1.0f,
|
||||||
|
1.0f,
|
||||||
|
0.97f,
|
||||||
|
0.77f,
|
||||||
|
0.84f,
|
||||||
|
1.0f
|
||||||
|
),
|
||||||
|
colors3 = floatArrayOf(
|
||||||
|
0.97f,
|
||||||
|
0.79f,
|
||||||
|
0.85f,
|
||||||
|
1.0f,
|
||||||
|
0.65f,
|
||||||
|
0.68f,
|
||||||
|
0.98f,
|
||||||
|
1.0f,
|
||||||
|
0.66f,
|
||||||
|
0.77f,
|
||||||
|
1.0f,
|
||||||
|
1.0f,
|
||||||
|
0.72f,
|
||||||
|
0.73f,
|
||||||
|
0.98f,
|
||||||
|
1.0f
|
||||||
|
),
|
||||||
|
colorInterpPeriod = 7.0f,
|
||||||
|
lightOffset = 0.1f,
|
||||||
|
saturateOffset = 0.2f,
|
||||||
|
pointOffset = 0.2f,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val OS3_PAD_DARK = BgEffectConfig.Config(
|
||||||
|
points = floatArrayOf(0.8f, 0.2f, 1.0f, 0.8f, 0.9f, 1.0f, 0.2f, 0.9f, 1.0f, 0.2f, 0.2f, 1.0f),
|
||||||
|
colors1 = floatArrayOf(
|
||||||
|
0.66f,
|
||||||
|
0.26f,
|
||||||
|
0.62f,
|
||||||
|
0.4f,
|
||||||
|
0.06f,
|
||||||
|
0.25f,
|
||||||
|
0.84f,
|
||||||
|
0.5f,
|
||||||
|
0.0f,
|
||||||
|
0.64f,
|
||||||
|
0.96f,
|
||||||
|
0.5f,
|
||||||
|
0.14f,
|
||||||
|
0.18f,
|
||||||
|
0.55f,
|
||||||
|
0.5f
|
||||||
|
),
|
||||||
|
colors2 = floatArrayOf(
|
||||||
|
0.07f,
|
||||||
|
0.15f,
|
||||||
|
0.79f,
|
||||||
|
0.5f,
|
||||||
|
0.11f,
|
||||||
|
0.16f,
|
||||||
|
0.83f,
|
||||||
|
0.5f,
|
||||||
|
0.06f,
|
||||||
|
0.25f,
|
||||||
|
0.84f,
|
||||||
|
0.5f,
|
||||||
|
0.66f,
|
||||||
|
0.26f,
|
||||||
|
0.62f,
|
||||||
|
0.5f
|
||||||
|
),
|
||||||
|
colors3 = floatArrayOf(
|
||||||
|
0.58f,
|
||||||
|
0.3f,
|
||||||
|
0.74f,
|
||||||
|
0.5f,
|
||||||
|
0.11f,
|
||||||
|
0.16f,
|
||||||
|
0.83f,
|
||||||
|
0.5f,
|
||||||
|
0.66f,
|
||||||
|
0.26f,
|
||||||
|
0.62f,
|
||||||
|
0.5f,
|
||||||
|
0.27f,
|
||||||
|
0.18f,
|
||||||
|
0.6f,
|
||||||
|
0.6f
|
||||||
|
),
|
||||||
|
colorInterpPeriod = 7.0f,
|
||||||
|
lightOffset = 0.0f,
|
||||||
|
saturateOffset = 0.0f,
|
||||||
|
pointOffset = 0.2f,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun getOs3Config(deviceType: DeviceType, isDark: Boolean): BgEffectConfig.Config {
|
||||||
|
return when (deviceType) {
|
||||||
|
DeviceType.PHONE -> if (isDark) OS3_PHONE_DARK else OS3_PHONE_LIGHT
|
||||||
|
DeviceType.PAD -> if (isDark) OS3_PAD_DARK else OS3_PAD_LIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.pages.effect
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import top.yukonga.miuix.kmp.blur.RuntimeShader
|
||||||
|
import top.yukonga.miuix.kmp.blur.asBrush
|
||||||
|
|
||||||
|
internal class BgEffectPainter(
|
||||||
|
private val isOs3: Boolean = true,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val runtimeShader by lazy {
|
||||||
|
val shaderCode = if (isOs3) OS3_BG_FRAG else OS2_BG_FRAG
|
||||||
|
RuntimeShader(shaderCode).also {
|
||||||
|
initStaticUniforms(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val brush: Brush by lazy { runtimeShader.asBrush() }
|
||||||
|
|
||||||
|
private val resolution = FloatArray(2)
|
||||||
|
private val bound = FloatArray(4)
|
||||||
|
|
||||||
|
private var animTime = Float.NaN
|
||||||
|
private var isDarkCached: Boolean? = null
|
||||||
|
private var deviceTypeCached: DeviceType? = null
|
||||||
|
|
||||||
|
private var presetApplied = false
|
||||||
|
|
||||||
|
private var deviceType = DeviceType.PHONE
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val U_TRANSLATE_Y = 0f
|
||||||
|
private const val U_ALPHA_MULTI = 1f
|
||||||
|
private const val U_NOISE_SCALE = 1.5f
|
||||||
|
private const val U_POINT_RADIUS_MULTI = 1f
|
||||||
|
private const val U_ALPHA_OFFSET = 0.1f
|
||||||
|
private const val U_SHADOW_OFFSET = 0.01f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initStaticUniforms(shader: RuntimeShader) {
|
||||||
|
shader.setFloatUniform("uTranslateY", U_TRANSLATE_Y)
|
||||||
|
shader.setFloatUniform("uNoiseScale", U_NOISE_SCALE)
|
||||||
|
shader.setFloatUniform("uPointRadiusMulti", U_POINT_RADIUS_MULTI)
|
||||||
|
shader.setFloatUniform("uAlphaMulti", U_ALPHA_MULTI)
|
||||||
|
|
||||||
|
if (isOs3) {
|
||||||
|
shader.setFloatUniform("uAlphaOffset", U_ALPHA_OFFSET)
|
||||||
|
shader.setFloatUniform("uShadowOffset", U_SHADOW_OFFSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDeviceType(type: DeviceType) {
|
||||||
|
if (deviceType == type) return
|
||||||
|
deviceType = type
|
||||||
|
presetApplied = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateResolution(width: Float, height: Float) {
|
||||||
|
if (resolution[0] == width && resolution[1] == height) return
|
||||||
|
resolution[0] = width
|
||||||
|
resolution[1] = height
|
||||||
|
runtimeShader.setFloatUniform("uResolution", resolution)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAnimTime(time: Float) {
|
||||||
|
if (animTime == time) return
|
||||||
|
animTime = time
|
||||||
|
runtimeShader.setFloatUniform("uAnimTime", animTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateColors(colors: FloatArray) {
|
||||||
|
runtimeShader.setFloatUniform("uColors", colors)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePresetIfNeeded(
|
||||||
|
logoHeight: Float,
|
||||||
|
height: Float,
|
||||||
|
width: Float,
|
||||||
|
isDark: Boolean,
|
||||||
|
) {
|
||||||
|
if (presetApplied && isDarkCached == isDark && deviceTypeCached == deviceType) return
|
||||||
|
|
||||||
|
updateBound(logoHeight, height, width)
|
||||||
|
applyPreset(isDark)
|
||||||
|
|
||||||
|
isDarkCached = isDark
|
||||||
|
deviceTypeCached = deviceType
|
||||||
|
presetApplied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyPreset(isDark: Boolean) {
|
||||||
|
val preset = BgEffectConfig.get(deviceType, isDark, isOs3)
|
||||||
|
|
||||||
|
runtimeShader.setFloatUniform("uPoints", preset.points)
|
||||||
|
runtimeShader.setFloatUniform("uPointOffset", preset.pointOffset)
|
||||||
|
runtimeShader.setFloatUniform("uLightOffset", preset.lightOffset)
|
||||||
|
runtimeShader.setFloatUniform("uSaturateOffset", preset.saturateOffset)
|
||||||
|
runtimeShader.setFloatUniform("uBound", bound)
|
||||||
|
|
||||||
|
if (isOs3) {
|
||||||
|
runtimeShader.setFloatUniform("uShadowColorMulti", preset.shadowColorMulti)
|
||||||
|
runtimeShader.setFloatUniform("uShadowColorOffset", preset.shadowColorOffset)
|
||||||
|
runtimeShader.setFloatUniform("uShadowNoiseScale", preset.shadowNoiseScale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateBound(
|
||||||
|
logoHeight: Float,
|
||||||
|
totalHeight: Float,
|
||||||
|
totalWidth: Float,
|
||||||
|
) {
|
||||||
|
val heightRatio = logoHeight / totalHeight
|
||||||
|
if (totalWidth <= totalHeight) {
|
||||||
|
bound[0] = 0f
|
||||||
|
bound[1] = 1f - heightRatio
|
||||||
|
bound[2] = 1f
|
||||||
|
bound[3] = heightRatio
|
||||||
|
} else {
|
||||||
|
val aspectRatio = totalWidth / totalHeight
|
||||||
|
val contentCenterY = 1f - heightRatio / 2f
|
||||||
|
bound[0] = 0f
|
||||||
|
bound[1] = contentCenterY - aspectRatio / 2f
|
||||||
|
bound[2] = 1f
|
||||||
|
bound[3] = aspectRatio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.pages.effect
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.withFrameNanos
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun rememberFrameTimeSeconds(
|
||||||
|
playing: Boolean = true,
|
||||||
|
): () -> Float {
|
||||||
|
var time by remember { mutableFloatStateOf(0f) }
|
||||||
|
var startOffset by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
|
LaunchedEffect(playing) {
|
||||||
|
if (!playing) {
|
||||||
|
startOffset = time
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
val start = withFrameNanos { it }
|
||||||
|
while (playing) {
|
||||||
|
val now = withFrameNanos { it }
|
||||||
|
time = startOffset + (now - start) / 1_000_000_000f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { time }
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.pages.effect
|
||||||
|
|
||||||
|
internal const val OS2_BG_FRAG = """
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
uniform shader uTex;
|
||||||
|
uniform float uAnimTime;
|
||||||
|
uniform vec4 uBound;
|
||||||
|
uniform float uTranslateY;
|
||||||
|
uniform vec3 uPoints[4];
|
||||||
|
uniform vec4 uColors[4];
|
||||||
|
uniform float uAlphaMulti;
|
||||||
|
uniform float uNoiseScale;
|
||||||
|
uniform float uPointOffset;
|
||||||
|
uniform float uPointRadiusMulti;
|
||||||
|
uniform float uSaturateOffset;
|
||||||
|
uniform float uLightOffset;
|
||||||
|
|
||||||
|
vec3 rgb2hsv(vec3 c)
|
||||||
|
{
|
||||||
|
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
||||||
|
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
|
||||||
|
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
|
||||||
|
float d = q.x - min(q.w, q.y);
|
||||||
|
float e = 1.0e-10;
|
||||||
|
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hsv2rgb(vec3 c)
|
||||||
|
{
|
||||||
|
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||||
|
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
||||||
|
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float hash(vec2 p) {
|
||||||
|
vec3 p3 = fract(vec3(p.xyx) * 0.13);
|
||||||
|
p3 += dot(p3, p3.yzx + 3.333);
|
||||||
|
return fract((p3.x + p3.y) * p3.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
float perlin(vec2 x) {
|
||||||
|
vec2 i = floor(x);
|
||||||
|
vec2 f = fract(x);
|
||||||
|
float a = hash(i);
|
||||||
|
float b = hash(i + vec2(1.0, 0.0));
|
||||||
|
float c = hash(i + vec2(0.0, 1.0));
|
||||||
|
float d = hash(i + vec2(1.0, 1.0));
|
||||||
|
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||||
|
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
float gradientNoise(in vec2 uv)
|
||||||
|
{
|
||||||
|
return fract(52.9829189 * fract(dot(uv, vec2(0.06711056, 0.00583715))));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 main(vec2 fragCoord){
|
||||||
|
vec2 vUv = fragCoord/uResolution;
|
||||||
|
vUv.y = 1.0-vUv.y;
|
||||||
|
vec2 uv = vUv;
|
||||||
|
uv -= vec2(0., uTranslateY);
|
||||||
|
uv.xy -= uBound.xy;
|
||||||
|
uv.xy /= uBound.zw;
|
||||||
|
vec3 hsv;
|
||||||
|
vec4 color = vec4(0.0);
|
||||||
|
float noiseValue = perlin(vUv * uNoiseScale + vec2(-uAnimTime, -uAnimTime));
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++){
|
||||||
|
vec4 pointColor = uColors[i];
|
||||||
|
pointColor.rgb *= pointColor.a;
|
||||||
|
vec2 point = uPoints[i].xy;
|
||||||
|
float rad = uPoints[i].z * uPointRadiusMulti;
|
||||||
|
point.x += sin(uAnimTime + point.y) * uPointOffset;
|
||||||
|
point.y += cos(uAnimTime + point.x) * uPointOffset;
|
||||||
|
float d = distance(uv, point);
|
||||||
|
float pct = smoothstep(rad, 0., d);
|
||||||
|
color.rgb = mix(color.rgb, pointColor.rgb, pct);
|
||||||
|
color.a = mix(color.a, pointColor.a, pct);
|
||||||
|
}
|
||||||
|
|
||||||
|
float oppositeNoise = smoothstep(0., 1., noiseValue);
|
||||||
|
if (color.a > 0.001) {
|
||||||
|
color.rgb /= color.a;
|
||||||
|
} else {
|
||||||
|
color.rgb = vec3(0.0);
|
||||||
|
}
|
||||||
|
hsv = rgb2hsv(color.rgb);
|
||||||
|
hsv.y = mix(hsv.y, 0.0, oppositeNoise * uSaturateOffset);
|
||||||
|
color.rgb = hsv2rgb(hsv);
|
||||||
|
color.rgb += oppositeNoise * uLightOffset;
|
||||||
|
color.a = clamp(color.a, 0., 1.);
|
||||||
|
color.a *= uAlphaMulti;
|
||||||
|
|
||||||
|
vec4 uiColor = uTex.eval(vec2(vUv.x, 1.0 - vUv.y)*uResolution);
|
||||||
|
vec4 fragColor;
|
||||||
|
color.rgb += (10.0 / 255.0) * gradientNoise(fragCoord.xy) - (5.0 / 255.0);
|
||||||
|
if (uiColor.a < 0.01) {
|
||||||
|
fragColor = color;
|
||||||
|
} else {
|
||||||
|
fragColor = uiColor;
|
||||||
|
}
|
||||||
|
return vec4(fragColor.rgb*fragColor.a, fragColor.a);
|
||||||
|
}
|
||||||
|
"""
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.pages.effect
|
||||||
|
|
||||||
|
internal const val OS3_BG_FRAG = """
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
uniform shader uTex;
|
||||||
|
uniform shader uTexBitmap;
|
||||||
|
uniform vec2 uTexWH;
|
||||||
|
|
||||||
|
uniform float uAnimTime;
|
||||||
|
uniform vec4 uBound;
|
||||||
|
uniform float uTranslateY;
|
||||||
|
uniform vec3 uPoints[4];
|
||||||
|
uniform vec4 uColors[4];
|
||||||
|
uniform float uAlphaMulti;
|
||||||
|
uniform float uNoiseScale;
|
||||||
|
uniform float uPointOffset;
|
||||||
|
uniform float uPointRadiusMulti;
|
||||||
|
uniform float uSaturateOffset;
|
||||||
|
uniform float uLightOffset;
|
||||||
|
uniform float uAlphaOffset;
|
||||||
|
uniform float uShadowColorMulti;
|
||||||
|
uniform float uShadowColorOffset;
|
||||||
|
uniform float uShadowNoiseScale;
|
||||||
|
uniform float uShadowOffset;
|
||||||
|
|
||||||
|
vec3 hsl2rgb(in vec3 c) {
|
||||||
|
vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0, 4.0, 2.0), 6.0)-3.0)-1.0, 0.0, 1.0);
|
||||||
|
return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 HueShift (in vec3 Color, in float Shift) {
|
||||||
|
vec3 P = vec3(0.55735)*dot(vec3(0.55735), Color);
|
||||||
|
vec3 U = Color-P;
|
||||||
|
vec3 V = cross(vec3(0.55735), U);
|
||||||
|
Color = U*cos(Shift*6.2832) + V*sin(Shift*6.2832) + P;
|
||||||
|
return vec3(Color);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 rgb2hsl(in vec3 c){
|
||||||
|
float h = 0.0; float s = 0.0; float l = 0.0;
|
||||||
|
float r = c.r; float g = c.g; float b = c.b;
|
||||||
|
float cMin = min(r, min(g, b));
|
||||||
|
float cMax = max(r, max(g, b));
|
||||||
|
l = (cMax + cMin) / 2.0;
|
||||||
|
if (cMax > cMin) {
|
||||||
|
float cDelta = cMax - cMin;
|
||||||
|
s = l < .0 ? cDelta / (cMax + cMin) : cDelta / (2.0 - (cMax + cMin));
|
||||||
|
if (r == cMax) h = (g - b) / cDelta;
|
||||||
|
else if (g == cMax) h = 2.0 + (b - r) / cDelta;
|
||||||
|
else h = 4.0 + (r - g) / cDelta;
|
||||||
|
if (h < 0.0) h += 6.0;
|
||||||
|
h = h / 6.0;
|
||||||
|
}
|
||||||
|
return vec3(h, s, l);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 rgb2hsv(vec3 c) {
|
||||||
|
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
||||||
|
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
|
||||||
|
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
|
||||||
|
float d = q.x - min(q.w, q.y);
|
||||||
|
float e = 1.0e-10;
|
||||||
|
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hsv2rgb(vec3 c) {
|
||||||
|
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||||
|
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
||||||
|
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float hash(vec2 p) {
|
||||||
|
vec3 p3 = fract(vec3(p.xyx) * 0.13);
|
||||||
|
p3 += dot(p3, p3.yzx + 3.333);
|
||||||
|
return fract((p3.x + p3.y) * p3.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
float perlin(vec2 x) {
|
||||||
|
vec2 i = floor(x); vec2 f = fract(x);
|
||||||
|
float a = hash(i); float b = hash(i + vec2(1.0, 0.0));
|
||||||
|
float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0));
|
||||||
|
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||||
|
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
float gradientNoise(in vec2 uv) {
|
||||||
|
return fract(52.9829189 * fract(dot(uv, vec2(0.06711056, 0.00583715))));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 main(vec2 fragCoord){
|
||||||
|
vec2 vUv = fragCoord/uResolution;
|
||||||
|
vUv.y = 1.0-vUv.y;
|
||||||
|
vec2 uv = vUv;
|
||||||
|
uv -= vec2(0., uTranslateY);
|
||||||
|
uv.xy -= uBound.xy;
|
||||||
|
uv.xy /= uBound.zw;
|
||||||
|
|
||||||
|
vec3 hsv;
|
||||||
|
vec4 color = vec4(0.0);
|
||||||
|
float noiseValue = perlin(vUv * uNoiseScale + vec2(-uAnimTime, -uAnimTime));
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++){
|
||||||
|
vec4 pointColor = uColors[i];
|
||||||
|
pointColor.rgb *= pointColor.a;
|
||||||
|
vec2 point = uPoints[i].xy;
|
||||||
|
float rad = uPoints[i].z * uPointRadiusMulti;
|
||||||
|
|
||||||
|
point.x += sin(uAnimTime + point.y) * uPointOffset;
|
||||||
|
point.y += cos(uAnimTime + point.x) * uPointOffset;
|
||||||
|
|
||||||
|
float d = distance(uv, point);
|
||||||
|
float pct = smoothstep(rad, 0., d);
|
||||||
|
|
||||||
|
color.rgb = mix(color.rgb, pointColor.rgb, pct);
|
||||||
|
color.a = mix(color.a, pointColor.a, pct);
|
||||||
|
}
|
||||||
|
|
||||||
|
float oppositeNoise = smoothstep(0., 1., noiseValue);
|
||||||
|
color.rgb /= color.a;
|
||||||
|
hsv = rgb2hsv(color.rgb);
|
||||||
|
hsv.y = mix(hsv.y, 0.0, oppositeNoise * uSaturateOffset);
|
||||||
|
color.rgb = hsv2rgb(hsv);
|
||||||
|
color.rgb += oppositeNoise * uLightOffset;
|
||||||
|
|
||||||
|
color.a = clamp(color.a, 0., 1.);
|
||||||
|
color.a *= uAlphaMulti;
|
||||||
|
|
||||||
|
vec4 uiColor = uTex.eval(vec2(vUv.x, 1.0 - vUv.y)*uResolution);
|
||||||
|
vec4 fragColor;
|
||||||
|
|
||||||
|
if (uiColor.a < 0.01) {
|
||||||
|
fragColor = color;
|
||||||
|
} else {
|
||||||
|
fragColor = uiColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
color += (1.0 / 255.0) * gradientNoise(fragCoord.xy) - (0.5 / 255.0);
|
||||||
|
return vec4(color.rgb*color.a, color.a);
|
||||||
|
}
|
||||||
|
"""
|
||||||
@@ -2,16 +2,12 @@ package io.github.miuzarte.scrcpyforandroid.password
|
|||||||
|
|
||||||
import android.app.KeyguardManager
|
import android.app.KeyguardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import io.github.miuzarte.scrcpyforandroid.nativecore.AdbMdnsDiscoverer
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
@@ -30,7 +26,8 @@ object BiometricGate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isDeviceSecure(): Boolean {
|
fun isDeviceSecure(): Boolean {
|
||||||
val manager = AppRuntime.context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
val manager = AppRuntime.context
|
||||||
|
.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||||
return manager.isDeviceSecure
|
return manager.isDeviceSecure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import top.yukonga.miuix.kmp.basic.SpinnerDefaults
|
|||||||
import top.yukonga.miuix.kmp.basic.SpinnerEntry
|
import top.yukonga.miuix.kmp.basic.SpinnerEntry
|
||||||
import top.yukonga.miuix.kmp.basic.SpinnerItemImpl
|
import top.yukonga.miuix.kmp.basic.SpinnerItemImpl
|
||||||
import top.yukonga.miuix.kmp.basic.Text
|
import top.yukonga.miuix.kmp.basic.Text
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PasswordPickerPopupContent(
|
fun PasswordPickerPopupContent(
|
||||||
@@ -67,7 +67,7 @@ fun PasswordPickerPopupContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(UiSpacing.PopupHorizontal),
|
.padding(UiSpacing.PopupHorizontal),
|
||||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
color = colorScheme.onSurfaceVariantSummary,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
)
|
)
|
||||||
return@ListPopupColumn
|
return@ListPopupColumn
|
||||||
@@ -79,7 +79,7 @@ fun PasswordPickerPopupContent(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(UiSpacing.PopupHorizontal),
|
.padding(UiSpacing.PopupHorizontal),
|
||||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
color = colorScheme.onSurfaceVariantSummary,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
)
|
)
|
||||||
return@ListPopupColumn
|
return@ListPopupColumn
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import androidx.core.content.edit
|
|||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKey
|
import androidx.security.crypto.MasterKey
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package io.github.miuzarte.scrcpyforandroid.scaffolds
|
|||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
@@ -14,6 +15,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||||
@@ -29,11 +31,20 @@ fun LazyColumn(
|
|||||||
itemSpacing: Dp = UiSpacing.PageItem,
|
itemSpacing: Dp = UiSpacing.PageItem,
|
||||||
horizontalPadding: Dp = UiSpacing.PageHorizontal,
|
horizontalPadding: Dp = UiSpacing.PageHorizontal,
|
||||||
verticalPadding: Dp = UiSpacing.PageVertical,
|
verticalPadding: Dp = UiSpacing.PageVertical,
|
||||||
|
bottomInnerPadding: Dp? = null,
|
||||||
clearFocusOnTap: Boolean = true,
|
clearFocusOnTap: Boolean = true,
|
||||||
content: LazyListScope.() -> Unit,
|
content: LazyListScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
val mergedContentPadding = PaddingValues(
|
||||||
|
start = contentPadding.calculateLeftPadding(layoutDirection) + horizontalPadding,
|
||||||
|
top = contentPadding.calculateTopPadding() + verticalPadding,
|
||||||
|
end = contentPadding.calculateRightPadding(layoutDirection) + horizontalPadding,
|
||||||
|
bottom = contentPadding.calculateBottomPadding() + verticalPadding,
|
||||||
|
)
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -48,11 +59,16 @@ fun LazyColumn(
|
|||||||
if (scrollBehavior != null)
|
if (scrollBehavior != null)
|
||||||
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
else Modifier
|
else Modifier
|
||||||
)
|
),
|
||||||
.padding(contentPadding),
|
|
||||||
state = state,
|
state = state,
|
||||||
contentPadding = PaddingValues(horizontal = horizontalPadding, vertical = verticalPadding),
|
contentPadding = mergedContentPadding,
|
||||||
verticalArrangement = Arrangement.spacedBy(itemSpacing),
|
verticalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||||
content = content,
|
) {
|
||||||
)
|
content()
|
||||||
|
bottomInnerPadding?.let { padding ->
|
||||||
|
(padding - itemSpacing)
|
||||||
|
.takeIf { it > 0.dp }
|
||||||
|
?.let { item { Spacer(Modifier.height(it)) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import top.yukonga.miuix.kmp.basic.Text
|
|||||||
import top.yukonga.miuix.kmp.basic.TextButton
|
import top.yukonga.miuix.kmp.basic.TextButton
|
||||||
import top.yukonga.miuix.kmp.overlay.OverlayDialog
|
import top.yukonga.miuix.kmp.overlay.OverlayDialog
|
||||||
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SuperSlider(
|
fun SuperSlider(
|
||||||
@@ -69,8 +70,8 @@ fun SuperSlider(
|
|||||||
val text = if (shouldShowUnit) "$valueText $unit" else valueText
|
val text = if (shouldShowUnit) "$valueText $unit" else valueText
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
fontSize = textStyles.body2.fontSize,
|
||||||
color = MiuixTheme.colorScheme.onSurfaceVariantActions,
|
color = colorScheme.onSurfaceVariantActions,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import top.yukonga.miuix.kmp.basic.TextField
|
import top.yukonga.miuix.kmp.basic.TextField
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapped [TextField] component with state change callbacks.
|
* A wrapped [TextField] component with state change callbacks.
|
||||||
@@ -44,7 +45,6 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme
|
|||||||
* @param onFocusLost The callback to be called when the text field loses focus.
|
* @param onFocusLost The callback to be called when the text field loses focus.
|
||||||
* @param insideMargin The margin inside the [TextField].
|
* @param insideMargin The margin inside the [TextField].
|
||||||
* @param backgroundColor The background color of the [TextField].
|
* @param backgroundColor The background color of the [TextField].
|
||||||
* @param cornerRadius The corner radius of the [TextField].
|
|
||||||
* @param labelColor The color of the label.
|
* @param labelColor The color of the label.
|
||||||
* @param borderColor The color of the border when the [TextField] is focused.
|
* @param borderColor The color of the border when the [TextField] is focused.
|
||||||
* @param textStyle The text style to be applied to the [TextField].
|
* @param textStyle The text style to be applied to the [TextField].
|
||||||
@@ -70,12 +70,11 @@ fun SuperTextField(
|
|||||||
onFocusGained: (() -> Unit)? = null,
|
onFocusGained: (() -> Unit)? = null,
|
||||||
onFocusLost: (() -> Unit)? = null,
|
onFocusLost: (() -> Unit)? = null,
|
||||||
insideMargin: DpSize = DpSize(16.dp, 16.dp),
|
insideMargin: DpSize = DpSize(16.dp, 16.dp),
|
||||||
backgroundColor: Color = MiuixTheme.colorScheme.secondaryContainer,
|
backgroundColor: Color = colorScheme.secondaryContainer,
|
||||||
cornerRadius: Dp = 16.dp,
|
labelColor: Color = colorScheme.onSecondaryContainer,
|
||||||
labelColor: Color = MiuixTheme.colorScheme.onSecondaryContainer,
|
borderColor: Color = colorScheme.primary,
|
||||||
borderColor: Color = MiuixTheme.colorScheme.primary,
|
textStyle: TextStyle = textStyles.main,
|
||||||
textStyle: TextStyle = MiuixTheme.textStyles.main,
|
cursorBrush: Brush = SolidColor(colorScheme.primary),
|
||||||
cursorBrush: Brush = SolidColor(MiuixTheme.colorScheme.primary),
|
|
||||||
leadingIcon: @Composable (() -> Unit)? = null,
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
trailingIcon: @Composable (() -> Unit)? = null,
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
@@ -109,7 +108,6 @@ fun SuperTextField(
|
|||||||
visualTransformation = visualTransformation,
|
visualTransformation = visualTransformation,
|
||||||
insideMargin = insideMargin,
|
insideMargin = insideMargin,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
cornerRadius = cornerRadius,
|
|
||||||
labelColor = labelColor,
|
labelColor = labelColor,
|
||||||
borderColor = borderColor,
|
borderColor = borderColor,
|
||||||
textStyle = textStyle,
|
textStyle = textStyle,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package io.github.miuzarte.scrcpyforandroid.services
|
package io.github.miuzarte.scrcpyforandroid.services
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
|
import io.github.miuzarte.scrcpyforandroid.models.ConnectionTarget
|
||||||
import io.github.miuzarte.scrcpyforandroid.nativecore.AdbMdnsDiscoverer
|
import io.github.miuzarte.scrcpyforandroid.nativecore.AdbMdnsDiscoverer
|
||||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||||
@@ -11,7 +9,6 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
|||||||
object AppRuntime {
|
object AppRuntime {
|
||||||
private lateinit var appContext: Context
|
private lateinit var appContext: Context
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
appContext = context.applicationContext
|
appContext = context.applicationContext
|
||||||
AdbMdnsDiscoverer.init(appContext)
|
AdbMdnsDiscoverer.init(appContext)
|
||||||
|
|||||||
@@ -5,15 +5,10 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.password.BiometricGate
|
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
// not working in MIUI
|
// not working in MIUI
|
||||||
class PictureInPictureActionReceiver : BroadcastReceiver() {
|
class PictureInPictureActionReceiver : BroadcastReceiver() {
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
if (intent?.action != ACTION_STOP_SCRCPY) return
|
if (intent?.action != ACTION_STOP_SCRCPY) return
|
||||||
val pendingResult = goAsync()
|
val pendingResult = goAsync()
|
||||||
|
|||||||
@@ -22,6 +22,22 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
|||||||
booleanPreferencesKey("monet"),
|
booleanPreferencesKey("monet"),
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
val BLUR = Pair(
|
||||||
|
booleanPreferencesKey("blur"),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
val FLOATING_BOTTOM_BAR = Pair(
|
||||||
|
booleanPreferencesKey("floating_bottom_bar"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
val FLOATING_BOTTOM_BAR_BLUR = Pair(
|
||||||
|
booleanPreferencesKey("floating_bottom_bar_blur"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
val SMOOTH_CORNER = Pair(
|
||||||
|
booleanPreferencesKey("smooth_corner"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
val FULLSCREEN_DEBUG_INFO = Pair(
|
val FULLSCREEN_DEBUG_INFO = Pair(
|
||||||
booleanPreferencesKey("fullscreen_debug_info"),
|
booleanPreferencesKey("fullscreen_debug_info"),
|
||||||
false
|
false
|
||||||
@@ -100,6 +116,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
|||||||
// Theme Settings
|
// Theme Settings
|
||||||
val themeBaseIndex by setting(THEME_BASE_INDEX)
|
val themeBaseIndex by setting(THEME_BASE_INDEX)
|
||||||
val monet by setting(MONET)
|
val monet by setting(MONET)
|
||||||
|
val blur by setting(BLUR)
|
||||||
|
val floatingBottomBar by setting(FLOATING_BOTTOM_BAR)
|
||||||
|
val floatingBottomBarBlur by setting(FLOATING_BOTTOM_BAR_BLUR)
|
||||||
|
val smoothCorner by setting(SMOOTH_CORNER)
|
||||||
|
|
||||||
// Scrcpy Settings
|
// Scrcpy Settings
|
||||||
val fullscreenDebugInfo by setting(FULLSCREEN_DEBUG_INFO)
|
val fullscreenDebugInfo by setting(FULLSCREEN_DEBUG_INFO)
|
||||||
@@ -129,6 +149,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
|||||||
data class Bundle(
|
data class Bundle(
|
||||||
val themeBaseIndex: Int,
|
val themeBaseIndex: Int,
|
||||||
val monet: Boolean,
|
val monet: Boolean,
|
||||||
|
val blur: Boolean,
|
||||||
|
val floatingBottomBar: Boolean,
|
||||||
|
val floatingBottomBarBlur: Boolean,
|
||||||
|
val smoothCorner: Boolean,
|
||||||
val fullscreenDebugInfo: Boolean,
|
val fullscreenDebugInfo: Boolean,
|
||||||
val showFullscreenVirtualButtons: Boolean,
|
val showFullscreenVirtualButtons: Boolean,
|
||||||
val showFullscreenFloatingButton: Boolean,
|
val showFullscreenFloatingButton: Boolean,
|
||||||
@@ -153,6 +177,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
|||||||
private val bundleFields = arrayOf(
|
private val bundleFields = arrayOf(
|
||||||
bundleField(THEME_BASE_INDEX) { bundle: Bundle -> bundle.themeBaseIndex },
|
bundleField(THEME_BASE_INDEX) { bundle: Bundle -> bundle.themeBaseIndex },
|
||||||
bundleField(MONET) { bundle: Bundle -> bundle.monet },
|
bundleField(MONET) { bundle: Bundle -> bundle.monet },
|
||||||
|
bundleField(BLUR) { bundle: Bundle -> bundle.blur },
|
||||||
|
bundleField(FLOATING_BOTTOM_BAR) { bundle: Bundle -> bundle.floatingBottomBar },
|
||||||
|
bundleField(FLOATING_BOTTOM_BAR_BLUR) { bundle: Bundle -> bundle.floatingBottomBarBlur },
|
||||||
|
bundleField(SMOOTH_CORNER) { bundle: Bundle -> bundle.smoothCorner },
|
||||||
bundleField(FULLSCREEN_DEBUG_INFO) { bundle: Bundle -> bundle.fullscreenDebugInfo },
|
bundleField(FULLSCREEN_DEBUG_INFO) { bundle: Bundle -> bundle.fullscreenDebugInfo },
|
||||||
bundleField(SHOW_FULLSCREEN_VIRTUAL_BUTTONS) { bundle: Bundle -> bundle.showFullscreenVirtualButtons },
|
bundleField(SHOW_FULLSCREEN_VIRTUAL_BUTTONS) { bundle: Bundle -> bundle.showFullscreenVirtualButtons },
|
||||||
bundleField(SHOW_FULLSCREEN_FLOATING_BUTTON) { bundle: Bundle -> bundle.showFullscreenFloatingButton },
|
bundleField(SHOW_FULLSCREEN_FLOATING_BUTTON) { bundle: Bundle -> bundle.showFullscreenFloatingButton },
|
||||||
@@ -178,6 +206,10 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
|||||||
private fun bundleFromPreferences(preferences: Preferences) = Bundle(
|
private fun bundleFromPreferences(preferences: Preferences) = Bundle(
|
||||||
themeBaseIndex = preferences.read(THEME_BASE_INDEX),
|
themeBaseIndex = preferences.read(THEME_BASE_INDEX),
|
||||||
monet = preferences.read(MONET),
|
monet = preferences.read(MONET),
|
||||||
|
blur = preferences.read(BLUR),
|
||||||
|
floatingBottomBar = preferences.read(FLOATING_BOTTOM_BAR),
|
||||||
|
floatingBottomBarBlur = preferences.read(FLOATING_BOTTOM_BAR_BLUR),
|
||||||
|
smoothCorner = preferences.read(SMOOTH_CORNER),
|
||||||
fullscreenDebugInfo = preferences.read(FULLSCREEN_DEBUG_INFO),
|
fullscreenDebugInfo = preferences.read(FULLSCREEN_DEBUG_INFO),
|
||||||
showFullscreenVirtualButtons = preferences.read(SHOW_FULLSCREEN_VIRTUAL_BUTTONS),
|
showFullscreenVirtualButtons = preferences.read(SHOW_FULLSCREEN_VIRTUAL_BUTTONS),
|
||||||
showFullscreenFloatingButton = preferences.read(SHOW_FULLSCREEN_FLOATING_BUTTON),
|
showFullscreenFloatingButton = preferences.read(SHOW_FULLSCREEN_FLOATING_BUTTON),
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import top.yukonga.miuix.kmp.blur.BlendColorEntry
|
||||||
|
import top.yukonga.miuix.kmp.blur.BlurColors
|
||||||
|
import top.yukonga.miuix.kmp.blur.LayerBackdrop
|
||||||
|
import top.yukonga.miuix.kmp.blur.isRenderEffectSupported
|
||||||
|
import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop
|
||||||
|
import top.yukonga.miuix.kmp.blur.textureBlur
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
|
|
||||||
|
val LocalEnableBlur = staticCompositionLocalOf { false }
|
||||||
|
val LocalEnableFloatingBottomBar = staticCompositionLocalOf { false }
|
||||||
|
val LocalEnableFloatingBottomBarBlur = staticCompositionLocalOf { false }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberBlurBackdrop(enableBlur: Boolean): LayerBackdrop? {
|
||||||
|
if (!enableBlur || !isRenderEffectSupported()) return null
|
||||||
|
val surfaceColor = colorScheme.surface
|
||||||
|
return rememberLayerBackdrop {
|
||||||
|
drawRect(surfaceColor)
|
||||||
|
drawContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BlurredBar(
|
||||||
|
backdrop: LayerBackdrop?,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
if (backdrop != null) Modifier.textureBlur(
|
||||||
|
backdrop = backdrop,
|
||||||
|
shape = RectangleShape,
|
||||||
|
blurRadius = 25f * LocalDensity.current.density,
|
||||||
|
colors = BlurColors(
|
||||||
|
blendColors = listOf(
|
||||||
|
BlendColorEntry(color = colorScheme.surface.copy(alpha = 0.87f)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else Modifier,
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.EaseOut
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastCoerceIn
|
||||||
|
import androidx.compose.ui.util.fastRoundToInt
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
|
import com.kyant.backdrop.Backdrop
|
||||||
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
|
import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
|
||||||
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
|
import com.kyant.backdrop.drawBackdrop
|
||||||
|
import com.kyant.backdrop.effects.blur
|
||||||
|
import com.kyant.backdrop.effects.lens
|
||||||
|
import com.kyant.backdrop.effects.vibrancy
|
||||||
|
import com.kyant.backdrop.highlight.Highlight
|
||||||
|
import com.kyant.backdrop.shadow.InnerShadow
|
||||||
|
import com.kyant.backdrop.shadow.Shadow
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.component.miuix.animation.DampedDragAnimation
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.component.miuix.animation.InteractiveHighlight
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.sign
|
||||||
|
|
||||||
|
val LocalFloatingBottomBarTabScale = staticCompositionLocalOf { { 1f } }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RowScope.FloatingBottomBarItem(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
val scale = LocalFloatingBottomBarTabScale.current
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = null,
|
||||||
|
indication = null,
|
||||||
|
role = Role.Tab,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f)
|
||||||
|
.graphicsLayer {
|
||||||
|
val currentScale = scale()
|
||||||
|
scaleX = currentScale
|
||||||
|
scaleY = currentScale
|
||||||
|
},
|
||||||
|
verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FloatingBottomBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selectedIndex: () -> Int,
|
||||||
|
onSelected: (index: Int) -> Unit,
|
||||||
|
backdrop: Backdrop,
|
||||||
|
tabsCount: Int,
|
||||||
|
isBlurEnabled: Boolean = true,
|
||||||
|
content: @Composable RowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
val isInLightTheme = colorScheme.background.luminance() >= 0.5f
|
||||||
|
val accentColor = colorScheme.primary
|
||||||
|
val containerColor = if (isBlurEnabled) {
|
||||||
|
colorScheme.surfaceContainer.copy(0.4f)
|
||||||
|
} else {
|
||||||
|
colorScheme.surfaceContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
val tabsBackdrop = rememberLayerBackdrop()
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||||
|
val animationScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var tabWidthPx by remember { mutableFloatStateOf(0f) }
|
||||||
|
var totalWidthPx by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
|
val offsetAnimation = remember { Animatable(0f) }
|
||||||
|
val panelOffset by remember(density) {
|
||||||
|
derivedStateOf {
|
||||||
|
if (totalWidthPx == 0f) {
|
||||||
|
0f
|
||||||
|
} else {
|
||||||
|
val fraction = (offsetAnimation.value / totalWidthPx).fastCoerceIn(-1f, 1f)
|
||||||
|
with(density) {
|
||||||
|
4f.dp.toPx() * fraction.sign * EaseOut.transform(abs(fraction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentIndex by remember(selectedIndex) { mutableIntStateOf(selectedIndex()) }
|
||||||
|
|
||||||
|
class DampedDragAnimationHolder {
|
||||||
|
var instance: DampedDragAnimation? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
val holder = remember { DampedDragAnimationHolder() }
|
||||||
|
|
||||||
|
val dampedDragAnimation = remember(animationScope, tabsCount, density, isLtr) {
|
||||||
|
DampedDragAnimation(
|
||||||
|
animationScope = animationScope,
|
||||||
|
initialValue = selectedIndex().toFloat(),
|
||||||
|
valueRange = 0f..(tabsCount - 1).toFloat(),
|
||||||
|
visibilityThreshold = 0.001f,
|
||||||
|
initialScale = 1f,
|
||||||
|
pressedScale = 78f / 56f,
|
||||||
|
canDrag = { offset ->
|
||||||
|
val anim = holder.instance ?: return@DampedDragAnimation true
|
||||||
|
if (tabWidthPx == 0f) return@DampedDragAnimation false
|
||||||
|
|
||||||
|
val currentValue = anim.value
|
||||||
|
val indicatorX = currentValue * tabWidthPx
|
||||||
|
val padding = with(density) { 4.dp.toPx() }
|
||||||
|
val globalTouchX = if (isLtr) {
|
||||||
|
val touchX = indicatorX + offset.x
|
||||||
|
padding + touchX
|
||||||
|
} else {
|
||||||
|
val touchX = totalWidthPx - padding - tabWidthPx - indicatorX + offset.x
|
||||||
|
touchX
|
||||||
|
}
|
||||||
|
globalTouchX in 0f..totalWidthPx
|
||||||
|
},
|
||||||
|
onDragStarted = {},
|
||||||
|
onDragStopped = {
|
||||||
|
val targetIndex = targetValue.fastRoundToInt().fastCoerceIn(0, tabsCount - 1)
|
||||||
|
currentIndex = targetIndex
|
||||||
|
animateToValue(targetIndex.toFloat())
|
||||||
|
animationScope.launch {
|
||||||
|
offsetAnimation.animateTo(0f, spring(1f, 300f, 0.5f))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrag = { _, dragAmount ->
|
||||||
|
if (tabWidthPx > 0) {
|
||||||
|
updateValue(
|
||||||
|
(targetValue + dragAmount.x / tabWidthPx * if (isLtr) 1f else -1f)
|
||||||
|
.fastCoerceIn(0f, (tabsCount - 1).toFloat())
|
||||||
|
)
|
||||||
|
animationScope.launch {
|
||||||
|
offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).also { holder.instance = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(selectedIndex) {
|
||||||
|
snapshotFlow { selectedIndex() }.collectLatest { currentIndex = it }
|
||||||
|
}
|
||||||
|
LaunchedEffect(dampedDragAnimation) {
|
||||||
|
snapshotFlow { currentIndex }.drop(1).collectLatest { index ->
|
||||||
|
dampedDragAnimation.animateToValue(index.toFloat())
|
||||||
|
onSelected(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val interactiveHighlight = remember(animationScope, tabWidthPx) {
|
||||||
|
InteractiveHighlight(
|
||||||
|
animationScope = animationScope,
|
||||||
|
position = { size, _ ->
|
||||||
|
Offset(
|
||||||
|
if (isLtr) {
|
||||||
|
(dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset
|
||||||
|
} else {
|
||||||
|
size.width - (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset
|
||||||
|
},
|
||||||
|
size.height / 2f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier.width(IntrinsicSize.Min),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.onGloballyPositioned { coords ->
|
||||||
|
totalWidthPx = coords.size.width.toFloat()
|
||||||
|
val contentWidthPx = totalWidthPx - with(density) { 8.dp.toPx() }
|
||||||
|
tabWidthPx = contentWidthPx / tabsCount
|
||||||
|
}
|
||||||
|
.graphicsLayer { translationX = panelOffset }
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
.drawBackdrop(
|
||||||
|
backdrop = backdrop,
|
||||||
|
shape = { CircleShape },
|
||||||
|
effects = {
|
||||||
|
if (isBlurEnabled) {
|
||||||
|
vibrancy()
|
||||||
|
blur(8f.dp.toPx())
|
||||||
|
lens(24f.dp.toPx(), 24f.dp.toPx())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
highlight = {
|
||||||
|
Highlight.Default.copy(alpha = if (isBlurEnabled) 1f else 0f)
|
||||||
|
},
|
||||||
|
shadow = {
|
||||||
|
Shadow.Default.copy(
|
||||||
|
color = Color.Black.copy(if (isInLightTheme) 0.1f else 0.2f),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
layerBlock = {
|
||||||
|
if (isBlurEnabled) {
|
||||||
|
val progress = dampedDragAnimation.pressProgress
|
||||||
|
val scale = lerp(1f, 1f + 16f.dp.toPx() / size.width, progress)
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrawSurface = { drawRect(containerColor) }
|
||||||
|
)
|
||||||
|
.then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier)
|
||||||
|
.height(64.dp)
|
||||||
|
.padding(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalFloatingBottomBarTabScale provides {
|
||||||
|
if (isBlurEnabled) lerp(1f, 1.2f, dampedDragAnimation.pressProgress)
|
||||||
|
else 1f
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.clearAndSetSemantics {}
|
||||||
|
.alpha(0f)
|
||||||
|
.layerBackdrop(tabsBackdrop)
|
||||||
|
.graphicsLayer { translationX = panelOffset }
|
||||||
|
.drawBackdrop(
|
||||||
|
backdrop = backdrop,
|
||||||
|
shape = { CircleShape },
|
||||||
|
effects = {
|
||||||
|
if (isBlurEnabled) {
|
||||||
|
val progress = dampedDragAnimation.pressProgress
|
||||||
|
vibrancy()
|
||||||
|
blur(8f.dp.toPx())
|
||||||
|
lens(24f.dp.toPx() * progress, 24f.dp.toPx() * progress)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
highlight = {
|
||||||
|
Highlight.Default.copy(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f)
|
||||||
|
},
|
||||||
|
onDrawSurface = { drawRect(containerColor) }
|
||||||
|
)
|
||||||
|
.then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier)
|
||||||
|
.height(56.dp)
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
.graphicsLayer(colorFilter = ColorFilter.tint(accentColor)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabWidthPx > 0f) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
val contentWidth = totalWidthPx - with(density) { 8.dp.toPx() }
|
||||||
|
val singleTabWidth = contentWidth / tabsCount
|
||||||
|
|
||||||
|
val progressOffset = dampedDragAnimation.value * singleTabWidth
|
||||||
|
|
||||||
|
translationX = if (isLtr) {
|
||||||
|
progressOffset + panelOffset
|
||||||
|
} else {
|
||||||
|
-progressOffset + panelOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.then(if (isBlurEnabled) interactiveHighlight.gestureModifier else Modifier)
|
||||||
|
.then(dampedDragAnimation.modifier)
|
||||||
|
.drawBackdrop(
|
||||||
|
backdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop),
|
||||||
|
shape = { CircleShape },
|
||||||
|
effects = {
|
||||||
|
if (isBlurEnabled) {
|
||||||
|
val progress = dampedDragAnimation.pressProgress
|
||||||
|
lens(10f.dp.toPx() * progress, 14f.dp.toPx() * progress, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
highlight = {
|
||||||
|
Highlight.Default.copy(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f)
|
||||||
|
},
|
||||||
|
shadow = { Shadow(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f) },
|
||||||
|
innerShadow = {
|
||||||
|
InnerShadow(
|
||||||
|
radius = 8f.dp * dampedDragAnimation.pressProgress,
|
||||||
|
alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f
|
||||||
|
)
|
||||||
|
},
|
||||||
|
layerBlock = {
|
||||||
|
if (isBlurEnabled) {
|
||||||
|
scaleX = dampedDragAnimation.scaleX
|
||||||
|
scaleY = dampedDragAnimation.scaleY
|
||||||
|
val velocity = dampedDragAnimation.velocity / 10f
|
||||||
|
scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f)
|
||||||
|
scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrawSurface = {
|
||||||
|
val progress =
|
||||||
|
if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f
|
||||||
|
drawRect(
|
||||||
|
color = if (isInLightTheme) {
|
||||||
|
Color.Black.copy(0.1f)
|
||||||
|
} else {
|
||||||
|
Color.White.copy(0.1f)
|
||||||
|
},
|
||||||
|
alpha = 1f - progress
|
||||||
|
)
|
||||||
|
drawRect(
|
||||||
|
Color.Black.copy(alpha = 0.03f * progress)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.height(56.dp)
|
||||||
|
.width(with(density) { ((totalWidthPx - 8.dp.toPx()) / tabsCount).toDp() })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.ui.component.miuix.animation
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.MutatorMutex
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.component.miuix.modifier.inspectDragGestures
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.android.awaitFrame
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class DampedDragAnimation(
|
||||||
|
private val animationScope: CoroutineScope,
|
||||||
|
val initialValue: Float,
|
||||||
|
val valueRange: ClosedRange<Float>,
|
||||||
|
val visibilityThreshold: Float,
|
||||||
|
val initialScale: Float,
|
||||||
|
val pressedScale: Float,
|
||||||
|
val canDrag: (Offset) -> Boolean = { true },
|
||||||
|
val onDragStarted: DampedDragAnimation.(position: Offset) -> Unit,
|
||||||
|
val onDragStopped: DampedDragAnimation.() -> Unit,
|
||||||
|
val onDrag: DampedDragAnimation.(size: IntSize, dragAmount: Offset) -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val valueAnimationSpec = spring(1f, 1000f, visibilityThreshold)
|
||||||
|
private val velocityAnimationSpec = spring(0.5f, 300f, visibilityThreshold * 10f)
|
||||||
|
private val pressProgressAnimationSpec = spring(1f, 1000f, 0.001f)
|
||||||
|
private val scaleXAnimationSpec = spring(0.6f, 250f, 0.001f)
|
||||||
|
private val scaleYAnimationSpec = spring(0.7f, 250f, 0.001f)
|
||||||
|
|
||||||
|
private val valueAnimation = Animatable(initialValue, visibilityThreshold)
|
||||||
|
private val velocityAnimation = Animatable(0f, 5f)
|
||||||
|
private val pressProgressAnimation = Animatable(0f, 0.001f)
|
||||||
|
private val scaleXAnimation = Animatable(initialScale, 0.001f)
|
||||||
|
private val scaleYAnimation = Animatable(initialScale, 0.001f)
|
||||||
|
|
||||||
|
private val mutatorMutex = MutatorMutex()
|
||||||
|
private val velocityTracker = VelocityTracker()
|
||||||
|
|
||||||
|
val value: Float get() = valueAnimation.value
|
||||||
|
val targetValue: Float get() = valueAnimation.targetValue
|
||||||
|
val pressProgress: Float get() = pressProgressAnimation.value
|
||||||
|
val scaleX: Float get() = scaleXAnimation.value
|
||||||
|
val scaleY: Float get() = scaleYAnimation.value
|
||||||
|
val velocity: Float get() = velocityAnimation.value
|
||||||
|
|
||||||
|
val modifier: Modifier = Modifier.pointerInput(Unit) {
|
||||||
|
inspectDragGestures(
|
||||||
|
onDragStart = { down ->
|
||||||
|
onDragStarted(down.position)
|
||||||
|
press()
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
onDragStopped()
|
||||||
|
release()
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
onDragStopped()
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
) { change, dragAmount ->
|
||||||
|
val position = change.position
|
||||||
|
val previousPosition = change.previousPosition
|
||||||
|
|
||||||
|
val isInside = canDrag(position)
|
||||||
|
val wasInside = canDrag(previousPosition)
|
||||||
|
|
||||||
|
if (isInside && wasInside) {
|
||||||
|
onDrag(size, dragAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun press() {
|
||||||
|
velocityTracker.resetTracking()
|
||||||
|
animationScope.launch {
|
||||||
|
launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) }
|
||||||
|
launch { scaleXAnimation.animateTo(pressedScale, scaleXAnimationSpec) }
|
||||||
|
launch { scaleYAnimation.animateTo(pressedScale, scaleYAnimationSpec) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
animationScope.launch {
|
||||||
|
awaitFrame()
|
||||||
|
if (value != targetValue) {
|
||||||
|
val threshold = (valueRange.endInclusive - valueRange.start) * 0.025f
|
||||||
|
snapshotFlow { valueAnimation.value }
|
||||||
|
.filter { abs(it - valueAnimation.targetValue) < threshold }
|
||||||
|
.first()
|
||||||
|
}
|
||||||
|
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
|
||||||
|
launch { scaleXAnimation.animateTo(initialScale, scaleXAnimationSpec) }
|
||||||
|
launch { scaleYAnimation.animateTo(initialScale, scaleYAnimationSpec) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateValue(value: Float) {
|
||||||
|
val targetValue = value.coerceIn(valueRange)
|
||||||
|
animationScope.launch {
|
||||||
|
launch {
|
||||||
|
valueAnimation.animateTo(
|
||||||
|
targetValue,
|
||||||
|
valueAnimationSpec
|
||||||
|
) { updateVelocity() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun animateToValue(value: Float) {
|
||||||
|
animationScope.launch {
|
||||||
|
mutatorMutex.mutate {
|
||||||
|
press()
|
||||||
|
val targetValue = value.coerceIn(valueRange)
|
||||||
|
launch { valueAnimation.animateTo(targetValue, valueAnimationSpec) }
|
||||||
|
if (velocity != 0f) {
|
||||||
|
launch { velocityAnimation.animateTo(0f, velocityAnimationSpec) }
|
||||||
|
}
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateVelocity() {
|
||||||
|
velocityTracker.addPosition(
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
Offset(value, 0f)
|
||||||
|
)
|
||||||
|
val targetVelocity =
|
||||||
|
velocityTracker.calculateVelocity().x / (valueRange.endInclusive - valueRange.start)
|
||||||
|
animationScope.launch { velocityAnimation.animateTo(targetVelocity, velocityAnimationSpec) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.ui.component.miuix.animation
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.RuntimeShader
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.VectorConverter
|
||||||
|
import androidx.compose.animation.core.VisibilityThreshold
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ShaderBrush
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.util.fastCoerceIn
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.ui.component.miuix.modifier.inspectDragGestures
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.intellij.lang.annotations.Language
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
class InteractiveHighlight(
|
||||||
|
val animationScope: CoroutineScope,
|
||||||
|
val position: (size: Size, offset: Offset) -> Offset = { _, offset -> offset },
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val pressProgressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
||||||
|
private val positionAnimationSpec = spring(0.5f, 300f, Offset.VisibilityThreshold)
|
||||||
|
|
||||||
|
private val pressProgressAnimation = Animatable(0f, 0.001f)
|
||||||
|
private val positionAnimation =
|
||||||
|
Animatable(Offset.Zero, Offset.VectorConverter, Offset.VisibilityThreshold)
|
||||||
|
|
||||||
|
private var startPosition = Offset.Zero
|
||||||
|
val offset: Offset get() = positionAnimation.value - startPosition
|
||||||
|
|
||||||
|
@Language("AGSL")
|
||||||
|
private val shader = RuntimeShader(
|
||||||
|
"""
|
||||||
|
uniform float2 size;
|
||||||
|
layout(color) uniform half4 color;
|
||||||
|
uniform float radius;
|
||||||
|
uniform float2 position;
|
||||||
|
|
||||||
|
half4 main(float2 coord) {
|
||||||
|
float dist = distance(coord, position);
|
||||||
|
float intensity = smoothstep(radius, radius * 0.5, dist);
|
||||||
|
return color * intensity;
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
|
||||||
|
val modifier: Modifier = Modifier.drawWithContent {
|
||||||
|
val progress = pressProgressAnimation.value
|
||||||
|
if (progress > 0f) {
|
||||||
|
drawRect(
|
||||||
|
Color.White.copy(0.06f * progress),
|
||||||
|
blendMode = BlendMode.Plus
|
||||||
|
)
|
||||||
|
shader.apply {
|
||||||
|
val shaderPosition = position(size, positionAnimation.value)
|
||||||
|
val posX = shaderPosition.x.fastCoerceIn(0f, size.width)
|
||||||
|
val poxY = shaderPosition.y.fastCoerceIn(0f, size.height)
|
||||||
|
setFloatUniform("size", size.width, size.height)
|
||||||
|
setColorUniform("color", Color.White.copy(0.12f * progress).toArgb())
|
||||||
|
setFloatUniform("radius", size.minDimension * 1.2f)
|
||||||
|
setFloatUniform("position", posX, poxY)
|
||||||
|
}
|
||||||
|
drawRect(
|
||||||
|
ShaderBrush(shader),
|
||||||
|
blendMode = BlendMode.Plus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
val gestureModifier: Modifier = Modifier.pointerInput(animationScope) {
|
||||||
|
inspectDragGestures(
|
||||||
|
onDragStart = { down ->
|
||||||
|
startPosition = down.position
|
||||||
|
animationScope.launch {
|
||||||
|
launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) }
|
||||||
|
launch { positionAnimation.snapTo(startPosition) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
animationScope.launch {
|
||||||
|
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
|
||||||
|
launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
animationScope.launch {
|
||||||
|
launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }
|
||||||
|
launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { change, _ ->
|
||||||
|
animationScope.launch { positionAnimation.snapTo(change.position) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package io.github.miuzarte.scrcpyforandroid.ui.component.miuix.modifier
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
|
import androidx.compose.ui.input.pointer.PointerId
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||||
|
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
|
||||||
|
import androidx.compose.ui.input.pointer.positionChange
|
||||||
|
import androidx.compose.ui.util.fastFirstOrNull
|
||||||
|
|
||||||
|
suspend fun PointerInputScope.inspectDragGestures(
|
||||||
|
onDragStart: (down: PointerInputChange) -> Unit = {},
|
||||||
|
onDragEnd: (change: PointerInputChange) -> Unit = {},
|
||||||
|
onDragCancel: () -> Unit = {},
|
||||||
|
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,
|
||||||
|
) {
|
||||||
|
awaitEachGesture {
|
||||||
|
val initialDown = awaitFirstDown(false, PointerEventPass.Initial)
|
||||||
|
val down = awaitFirstDown(false)
|
||||||
|
|
||||||
|
onDragStart(down)
|
||||||
|
onDrag(initialDown, Offset.Zero)
|
||||||
|
val upEvent = drag(
|
||||||
|
pointerId = initialDown.id,
|
||||||
|
onDrag = { onDrag(it, it.positionChange()) }
|
||||||
|
)
|
||||||
|
if (upEvent == null) {
|
||||||
|
onDragCancel()
|
||||||
|
} else {
|
||||||
|
onDragEnd(upEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun AwaitPointerEventScope.drag(
|
||||||
|
pointerId: PointerId,
|
||||||
|
onDrag: (PointerInputChange) -> Unit,
|
||||||
|
): PointerInputChange? {
|
||||||
|
val isPointerUp = currentEvent.changes.fastFirstOrNull { it.id == pointerId }?.pressed != true
|
||||||
|
if (isPointerUp) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
var pointer = pointerId
|
||||||
|
while (true) {
|
||||||
|
val change = awaitDragOrUp(pointer) ?: return null
|
||||||
|
if (change.isConsumed) return null
|
||||||
|
if (change.changedToUpIgnoreConsumed()) return change
|
||||||
|
|
||||||
|
onDrag(change)
|
||||||
|
pointer = change.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun AwaitPointerEventScope.awaitDragOrUp(
|
||||||
|
pointerId: PointerId,
|
||||||
|
): PointerInputChange? {
|
||||||
|
var pointer = pointerId
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent()
|
||||||
|
val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
|
||||||
|
if (dragEvent.changedToUpIgnoreConsumed()) {
|
||||||
|
val otherDown = event.changes.fastFirstOrNull { it.pressed }
|
||||||
|
if (otherDown == null) {
|
||||||
|
return dragEvent
|
||||||
|
}
|
||||||
|
pointer = otherDown.id
|
||||||
|
} else {
|
||||||
|
val hasDragged = dragEvent.previousPosition != dragEvent.position
|
||||||
|
if (hasDragged) {
|
||||||
|
return dragEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,7 @@ import io.github.miuzarte.scrcpyforandroid.NativeCoreFacade
|
|||||||
import io.github.miuzarte.scrcpyforandroid.constants.Defaults
|
import io.github.miuzarte.scrcpyforandroid.constants.Defaults
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets
|
import io.github.miuzarte.scrcpyforandroid.constants.ScrcpyPresets
|
||||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.haptics.LocalAppHaptics
|
||||||
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
|
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
|
||||||
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcut
|
||||||
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider
|
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperSlider
|
||||||
@@ -101,7 +102,7 @@ import top.yukonga.miuix.kmp.preference.CheckboxLocation
|
|||||||
import top.yukonga.miuix.kmp.preference.CheckboxPreference
|
import top.yukonga.miuix.kmp.preference.CheckboxPreference
|
||||||
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
||||||
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme.isDynamicColor
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.isDynamicColor
|
||||||
import top.yukonga.miuix.kmp.utils.PressFeedbackType
|
import top.yukonga.miuix.kmp.utils.PressFeedbackType
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -132,17 +133,17 @@ internal fun StatusCard(
|
|||||||
val spec = when {
|
val spec = when {
|
||||||
streaming && sessionInfo != null -> {
|
streaming && sessionInfo != null -> {
|
||||||
val streamCardColor = when {
|
val streamCardColor = when {
|
||||||
isDynamicColor -> MiuixTheme.colorScheme.secondaryContainer
|
isDynamicColor -> colorScheme.secondaryContainer
|
||||||
isDarkTheme -> Color(0xFF1A3825)
|
isDarkTheme -> Color(0xFF1A3825)
|
||||||
else -> Color(0xFFDFFAE4)
|
else -> Color(0xFFDFFAE4)
|
||||||
}
|
}
|
||||||
val streamTextColor = when {
|
val streamTextColor = when {
|
||||||
isDynamicColor -> MiuixTheme.colorScheme.onSecondaryContainer
|
isDynamicColor -> colorScheme.onSecondaryContainer
|
||||||
isDarkTheme -> Color.White
|
isDarkTheme -> Color.White
|
||||||
else -> Color(0xFF111111)
|
else -> Color(0xFF111111)
|
||||||
}
|
}
|
||||||
val streamIconColor = if (isDynamicColor) {
|
val streamIconColor = if (isDynamicColor) {
|
||||||
MiuixTheme.colorScheme.primary.copy(alpha = 0.8f)
|
colorScheme.primary.copy(alpha = 0.8f)
|
||||||
} else {
|
} else {
|
||||||
Color(0xFF36D167)
|
Color(0xFF36D167)
|
||||||
}
|
}
|
||||||
@@ -171,11 +172,11 @@ internal fun StatusCard(
|
|||||||
big = StatusBigCardSpec(
|
big = StatusBigCardSpec(
|
||||||
title = "ADB 已连接",
|
title = "ADB 已连接",
|
||||||
subtitle = cleanStatusLine,
|
subtitle = cleanStatusLine,
|
||||||
containerColor = MiuixTheme.colorScheme.primaryContainer,
|
containerColor = colorScheme.primaryContainer,
|
||||||
titleColor = MiuixTheme.colorScheme.onPrimaryContainer,
|
titleColor = colorScheme.onPrimaryContainer,
|
||||||
subtitleColor = MiuixTheme.colorScheme.onPrimaryContainer,
|
subtitleColor = colorScheme.onPrimaryContainer,
|
||||||
icon = Icons.Rounded.Wifi,
|
icon = Icons.Rounded.Wifi,
|
||||||
iconTint = MiuixTheme.colorScheme.primary.copy(alpha = 0.6f),
|
iconTint = colorScheme.primary.copy(alpha = 0.6f),
|
||||||
),
|
),
|
||||||
firstSmall = StatusSmallCardSpec(
|
firstSmall = StatusSmallCardSpec(
|
||||||
"当前设备",
|
"当前设备",
|
||||||
@@ -191,11 +192,11 @@ internal fun StatusCard(
|
|||||||
big = StatusBigCardSpec(
|
big = StatusBigCardSpec(
|
||||||
title = "ADB 未连接",
|
title = "ADB 未连接",
|
||||||
subtitle = "",
|
subtitle = "",
|
||||||
containerColor = MiuixTheme.colorScheme.secondaryContainer,
|
containerColor = colorScheme.secondaryContainer,
|
||||||
titleColor = MiuixTheme.colorScheme.onSecondaryContainer,
|
titleColor = colorScheme.onSecondaryContainer,
|
||||||
subtitleColor = MiuixTheme.colorScheme.onSecondaryContainer,
|
subtitleColor = colorScheme.onSecondaryContainer,
|
||||||
icon = Icons.Rounded.LinkOff,
|
icon = Icons.Rounded.LinkOff,
|
||||||
iconTint = MiuixTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.6f),
|
iconTint = colorScheme.onSecondaryContainer.copy(alpha = 0.6f),
|
||||||
),
|
),
|
||||||
firstSmall = StatusSmallCardSpec(
|
firstSmall = StatusSmallCardSpec(
|
||||||
"当前设备",
|
"当前设备",
|
||||||
@@ -983,8 +984,8 @@ internal fun DeviceTile(
|
|||||||
Card(
|
Card(
|
||||||
colors = CardDefaults.defaultColors(
|
colors = CardDefaults.defaultColors(
|
||||||
color =
|
color =
|
||||||
if (isConnected) MiuixTheme.colorScheme.surfaceContainer
|
if (isConnected) colorScheme.surfaceContainer
|
||||||
else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f),
|
else colorScheme.surfaceContainer.copy(alpha = 0.6f),
|
||||||
),
|
),
|
||||||
pressFeedbackType = if (!editing) PressFeedbackType.Sink else PressFeedbackType.None,
|
pressFeedbackType = if (!editing) PressFeedbackType.Sink else PressFeedbackType.None,
|
||||||
onClick = haptics.contextClick,
|
onClick = haptics.contextClick,
|
||||||
@@ -1015,7 +1016,7 @@ internal fun DeviceTile(
|
|||||||
.background(
|
.background(
|
||||||
color =
|
color =
|
||||||
if (isConnected) Color(0xFF44C74F)
|
if (isConnected) Color(0xFF44C74F)
|
||||||
else MiuixTheme.colorScheme.outline,
|
else colorScheme.outline,
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1027,12 +1028,12 @@ internal fun DeviceTile(
|
|||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = MiuixTheme.colorScheme.onSurface,
|
color = colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"${device.host}:${device.port}",
|
"${device.host}:${device.port}",
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
color = colorScheme.onSurfaceVariantSummary,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
@@ -1206,14 +1207,16 @@ internal fun QuickConnectCard(
|
|||||||
onAddDevice: () -> Unit,
|
onAddDevice: () -> Unit,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
|
val haptics = LocalAppHaptics.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
colors = CardDefaults.defaultColors(color = MiuixTheme.colorScheme.primaryContainer),
|
colors = CardDefaults.defaultColors(color = colorScheme.primaryContainer),
|
||||||
pressFeedbackType =
|
pressFeedbackType =
|
||||||
if (enabled) PressFeedbackType.Tilt
|
if (enabled) PressFeedbackType.Tilt
|
||||||
else PressFeedbackType.None,
|
else PressFeedbackType.None,
|
||||||
insideMargin = PaddingValues(UiSpacing.Content),
|
insideMargin = PaddingValues(UiSpacing.Content),
|
||||||
|
onClick = haptics.contextClick,
|
||||||
) {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical)) {
|
Column(verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical)) {
|
||||||
Row(
|
Row(
|
||||||
@@ -1223,12 +1226,12 @@ internal fun QuickConnectCard(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.AddLink,
|
Icons.Rounded.AddLink,
|
||||||
contentDescription = "快速连接",
|
contentDescription = "快速连接",
|
||||||
tint = MiuixTheme.colorScheme.onPrimaryContainer,
|
tint = colorScheme.onPrimaryContainer,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"快速连接",
|
"快速连接",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MiuixTheme.colorScheme.onPrimaryContainer,
|
color = colorScheme.onPrimaryContainer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SuperTextField(
|
SuperTextField(
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import top.yukonga.miuix.kmp.basic.Card
|
|||||||
import top.yukonga.miuix.kmp.basic.Checkbox
|
import top.yukonga.miuix.kmp.basic.Checkbox
|
||||||
import top.yukonga.miuix.kmp.basic.Icon
|
import top.yukonga.miuix.kmp.basic.Icon
|
||||||
import top.yukonga.miuix.kmp.basic.Text
|
import top.yukonga.miuix.kmp.basic.Text
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
|
|
||||||
class ReorderableList(
|
class ReorderableList(
|
||||||
private val itemsProvider: () -> List<Item>,
|
private val itemsProvider: () -> List<Item>,
|
||||||
@@ -114,12 +114,12 @@ class ReorderableList(
|
|||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = item.title,
|
text = item.title,
|
||||||
color = MiuixTheme.colorScheme.onSurface,
|
color = colorScheme.onSurface,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
if (item.subtitle.isNotBlank()) Text(
|
if (item.subtitle.isNotBlank()) Text(
|
||||||
text = item.subtitle,
|
text = item.subtitle,
|
||||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
color = colorScheme.onSurfaceVariantSummary,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ class ReorderableList(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.DragIndicator,
|
Icons.Rounded.DragIndicator,
|
||||||
contentDescription = "拖动排序",
|
contentDescription = "拖动排序",
|
||||||
tint = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
tint = colorScheme.onSurfaceVariantSummary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,7 +218,7 @@ class ReorderableList(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.DragIndicator,
|
Icons.Rounded.DragIndicator,
|
||||||
contentDescription = "拖动排序",
|
contentDescription = "拖动排序",
|
||||||
tint = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
tint = colorScheme.onSurfaceVariantSummary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,13 +226,13 @@ class ReorderableList(
|
|||||||
Spacer(Modifier.padding(UiSpacing.ContentVertical))
|
Spacer(Modifier.padding(UiSpacing.ContentVertical))
|
||||||
Text(
|
Text(
|
||||||
text = item.title,
|
text = item.title,
|
||||||
color = MiuixTheme.colorScheme.onSurface,
|
color = colorScheme.onSurface,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
if (item.subtitle.isNotBlank()) {
|
if (item.subtitle.isNotBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = item.subtitle,
|
text = item.subtitle,
|
||||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
color = colorScheme.onSurfaceVariantSummary,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -266,7 +266,7 @@ private fun EndActionView(
|
|||||||
contentDescription = action.contentDescription.ifBlank {
|
contentDescription = action.contentDescription.ifBlank {
|
||||||
fallbackContentDescription
|
fallbackContentDescription
|
||||||
},
|
},
|
||||||
tint = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
tint = colorScheme.onSurfaceVariantSummary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import top.yukonga.miuix.kmp.basic.Card
|
|||||||
import top.yukonga.miuix.kmp.basic.CardDefaults.defaultColors
|
import top.yukonga.miuix.kmp.basic.CardDefaults.defaultColors
|
||||||
import top.yukonga.miuix.kmp.basic.Icon
|
import top.yukonga.miuix.kmp.basic.Icon
|
||||||
import top.yukonga.miuix.kmp.basic.Text
|
import top.yukonga.miuix.kmp.basic.Text
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
import top.yukonga.miuix.kmp.utils.PressFeedbackType
|
import top.yukonga.miuix.kmp.utils.PressFeedbackType
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -122,7 +122,7 @@ internal fun StatusCardLayout(
|
|||||||
Text(
|
Text(
|
||||||
text = busyLabel,
|
text = busyLabel,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = MiuixTheme.colorScheme.primary,
|
color = colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,13 +164,13 @@ private fun StatusMetricCard(spec: StatusSmallCardSpec, modifier: Modifier) {
|
|||||||
text = spec.title,
|
text = spec.title,
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
color = colorScheme.onSurfaceVariantSummary,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = spec.value,
|
text = spec.value,
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = MiuixTheme.colorScheme.onSurface,
|
color = colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ import top.yukonga.miuix.kmp.basic.SpinnerEntry
|
|||||||
import top.yukonga.miuix.kmp.basic.SpinnerItemImpl
|
import top.yukonga.miuix.kmp.basic.SpinnerItemImpl
|
||||||
import top.yukonga.miuix.kmp.basic.Text
|
import top.yukonga.miuix.kmp.basic.Text
|
||||||
import top.yukonga.miuix.kmp.overlay.OverlayListPopup
|
import top.yukonga.miuix.kmp.overlay.OverlayListPopup
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||||
|
|
||||||
enum class VirtualButtonAction(
|
enum class VirtualButtonAction(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -216,10 +216,10 @@ class VirtualButtonBar(
|
|||||||
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val haptics = LocalAppHaptics.current
|
val haptics = LocalAppHaptics.current
|
||||||
val activeContainerColor = MiuixTheme.colorScheme.primary
|
val activeContainerColor = colorScheme.primary
|
||||||
val disabledContainerColor = MiuixTheme.colorScheme.primary.copy(alpha = 0.35f)
|
val disabledContainerColor = colorScheme.primary.copy(alpha = 0.35f)
|
||||||
val activeContentColor = MiuixTheme.colorScheme.onPrimary
|
val activeContentColor = colorScheme.onPrimary
|
||||||
val disabledContentColor = MiuixTheme.colorScheme.onPrimary.copy(alpha = 0.45f)
|
val disabledContentColor = colorScheme.onPrimary.copy(alpha = 0.45f)
|
||||||
|
|
||||||
var showMorePopup by remember { mutableStateOf(false) }
|
var showMorePopup by remember { mutableStateOf(false) }
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ junit = "4.13.2"
|
|||||||
androidxJunit = "1.3.0"
|
androidxJunit = "1.3.0"
|
||||||
espressoCore = "3.7.0"
|
espressoCore = "3.7.0"
|
||||||
miuix = "0.9.0"
|
miuix = "0.9.0"
|
||||||
|
backdrop = "1.0.6"
|
||||||
material = "1.13.0"
|
material = "1.13.0"
|
||||||
runtime = "1.10.5"
|
runtime = "1.10.5"
|
||||||
androidxCorePip = "1.0.0-alpha02"
|
androidxCorePip = "1.0.0-alpha02"
|
||||||
@@ -38,6 +39,7 @@ miuix-blur = { group = "top.yukonga.miuix.kmp", name = "miuix-blur", version.ref
|
|||||||
miuix-preference = { group = "top.yukonga.miuix.kmp", name = "miuix-preference", version.ref = "miuix" }
|
miuix-preference = { group = "top.yukonga.miuix.kmp", name = "miuix-preference", version.ref = "miuix" }
|
||||||
miuix-icons = { group = "top.yukonga.miuix.kmp", name = "miuix-icons", version.ref = "miuix" }
|
miuix-icons = { group = "top.yukonga.miuix.kmp", name = "miuix-icons", version.ref = "miuix" }
|
||||||
miuix-navigation3-ui = { group = "top.yukonga.miuix.kmp", name = "miuix-navigation3-ui", version.ref = "miuix" }
|
miuix-navigation3-ui = { group = "top.yukonga.miuix.kmp", name = "miuix-navigation3-ui", version.ref = "miuix" }
|
||||||
|
backdrop = { group = "io.github.kyant0", name = "backdrop", version.ref = "backdrop" }
|
||||||
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
|
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
|
||||||
androidx-core-pip = { group = "androidx.core", name = "core-pip", version.ref = "androidxCorePip" }
|
androidx-core-pip = { group = "androidx.core", name = "core-pip", version.ref = "androidxCorePip" }
|
||||||
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidxBiometric" }
|
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidxBiometric" }
|
||||||
|
|||||||
Reference in New Issue
Block a user