improve: complete haptic feedback

This commit is contained in:
Miuzarte
2026-05-31 17:47:03 +08:00
parent b46a2d2d1f
commit 7f256882a4
9 changed files with 597 additions and 664 deletions

View File

@@ -6,14 +6,7 @@ import android.os.Bundle
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -22,21 +15,12 @@ import androidx.compose.material.icons.rounded.Block
import androidx.compose.material.icons.rounded.DeleteOutline import androidx.compose.material.icons.rounded.DeleteOutline
import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Password import androidx.compose.material.icons.rounded.Password
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
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.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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
@@ -45,11 +29,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.password.BiometricGate import io.github.miuzarte.scrcpyforandroid.password.*
import io.github.miuzarte.scrcpyforandroid.password.PasswordCreatedState
import io.github.miuzarte.scrcpyforandroid.password.PasswordEntry
import io.github.miuzarte.scrcpyforandroid.password.PasswordRepository
import io.github.miuzarte.scrcpyforandroid.password.PasswordSanitizer
import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
import io.github.miuzarte.scrcpyforandroid.scaffolds.ReorderableList import io.github.miuzarte.scrcpyforandroid.scaffolds.ReorderableList
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
@@ -57,28 +37,12 @@ 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.confirm
import io.github.miuzarte.scrcpyforandroid.ui.contextClick
import io.github.miuzarte.scrcpyforandroid.ui.createThemeController import io.github.miuzarte.scrcpyforandroid.ui.createThemeController
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers import top.yukonga.miuix.kmp.basic.*
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.ButtonDefaults
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.DropdownEntry
import top.yukonga.miuix.kmp.basic.DropdownItem
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.SnackbarHost
import top.yukonga.miuix.kmp.basic.SnackbarHostState
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.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
@@ -93,7 +57,7 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
class LockscreenPasswordActivity : FragmentActivity() { class LockscreenPasswordActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
@@ -154,6 +118,8 @@ private fun LockscreenPasswordScreen(
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
val scrollBehavior = MiuixScrollBehavior() val scrollBehavior = MiuixScrollBehavior()
val haptic = LocalHapticFeedback.current
val asBundleShared by appSettings.bundleState.collectAsState() val asBundleShared by appSettings.bundleState.collectAsState()
val asBundleSharedLatest by rememberUpdatedState(asBundleShared) val asBundleSharedLatest by rememberUpdatedState(asBundleShared)
var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) } var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) }
@@ -232,10 +198,15 @@ private fun LockscreenPasswordScreen(
if (blurBackdrop != null) Color.Transparent if (blurBackdrop != null) Color.Transparent
else colorScheme.surface, else colorScheme.surface,
navigationIcon = { navigationIcon = {
IconButton(onClick = { activity.onBackPressedDispatcher.onBackPressed() }) { IconButton(
onClick = {
haptic.contextClick()
activity.onBackPressedDispatcher.onBackPressed()
},
) {
Icon( Icon(
Icons.AutoMirrored.Rounded.ArrowBack, Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = stringResource(R.string.cd_back) contentDescription = stringResource(R.string.cd_back),
) )
} }
}, },
@@ -248,9 +219,9 @@ private fun LockscreenPasswordScreen(
text = textCreateNew, text = textCreateNew,
onClick = { onClick = {
pendingCreate = true pendingCreate = true
} },
) ),
) ),
), ),
) { ) {
Icon( Icon(
@@ -300,80 +271,86 @@ private fun LockscreenPasswordScreen(
}, },
) )
if (showRiskDialog) { OverlayDialog(
OverlayDialog( show = showRiskDialog,
show = true, title = stringResource(R.string.password_no_lock_screen),
title = stringResource(R.string.password_no_lock_screen), summary = stringResource(R.string.password_no_lock_screen_warn),
summary = stringResource(R.string.password_no_lock_screen_warn), defaultWindowInsetsPadding = false,
defaultWindowInsetsPadding = false, onDismissRequest = activity::finish,
onDismissRequest = activity::finish, ) {
) { Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal)) {
Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal)) { TextButton(
TextButton( text = stringResource(R.string.button_cancel),
text = stringResource(R.string.button_cancel), onClick = {
onClick = activity::finish, haptic.contextClick()
modifier = Modifier.weight(1f), activity.finish()
) },
TextButton( modifier = Modifier.weight(1f),
text = stringResource(R.string.password_agree), )
onClick = { showRiskDialog = false }, TextButton(
modifier = Modifier.weight(1f), text = stringResource(R.string.password_agree),
colors = ButtonDefaults.textButtonColorsPrimary(), onClick = {
) haptic.confirm()
} showRiskDialog = false
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary(),
)
} }
} }
if (showDisableDialog) { OverlayDialog(
OverlayDialog( show = showDisableDialog,
show = true, title = stringResource(R.string.password_auth_lost_warn),
title = stringResource(R.string.password_auth_lost_warn), summary = stringResource(R.string.password_auth_lost_detail),
summary = stringResource(R.string.password_auth_lost_detail), defaultWindowInsetsPadding = false,
defaultWindowInsetsPadding = false, onDismissRequest = { showDisableDialog = false },
onDismissRequest = { showDisableDialog = false }, ) {
) { Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal)) {
Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal)) { TextButton(
TextButton( text = stringResource(R.string.button_cancel),
text = stringResource(R.string.button_cancel), onClick = {
onClick = { showDisableDialog = false }, haptic.contextClick()
modifier = Modifier.weight(1f), showDisableDialog = false
) },
val textAuthToDisable = stringResource(R.string.password_auth_to_disable) modifier = Modifier.weight(1f),
val textAuthSubtitle = stringResource(R.string.password_auth_subtitle) )
TextButton( val textAuthToDisable = stringResource(R.string.password_auth_to_disable)
text = stringResource(R.string.password_continue_disable), val textAuthSubtitle = stringResource(R.string.password_auth_subtitle)
onClick = { TextButton(
scope.launch { text = stringResource(R.string.password_continue_disable),
if (entries.any { it.cipherText != null }) { onClick = {
val ok = BiometricGate.authenticate( haptic.confirm()
activity = activity, scope.launch {
title = textAuthToDisable, if (entries.any { it.cipherText != null }) {
subtitle = textAuthSubtitle, val ok = BiometricGate.authenticate(
) activity = activity,
if (!ok) { title = textAuthToDisable,
AppRuntime.snackbar(R.string.password_auth_failed) subtitle = textAuthSubtitle,
showDisableDialog = false )
return@launch if (!ok) {
} AppRuntime.snackbar(R.string.password_auth_failed)
showDisableDialog = false
return@launch
} }
asBundle = asBundle.copy(passwordRequireAuth = false)
entries.forEach { entry ->
PasswordRepository.update(
entry.copy(
cipherText = entry.cipherText?.copyOf(),
createdWithAuth = entry.createdWithAuth
.takeIf { it != PasswordCreatedState.AuthenticatedCreated }
?: PasswordCreatedState.AuthenticatedCreatedModified
)
)
}
showDisableDialog = false
} }
}, asBundle = asBundle.copy(passwordRequireAuth = false)
modifier = Modifier.weight(1f), entries.forEach { entry ->
colors = ButtonDefaults.textButtonColorsPrimary(), PasswordRepository.update(
) entry.copy(
} cipherText = entry.cipherText?.copyOf(),
createdWithAuth = entry.createdWithAuth
.takeIf { it != PasswordCreatedState.AuthenticatedCreated }
?: PasswordCreatedState.AuthenticatedCreatedModified,
),
)
}
showDisableDialog = false
}
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary(),
)
} }
} }
@@ -390,12 +367,16 @@ private fun LockscreenPasswordScreen(
Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal)) { Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal)) {
TextButton( TextButton(
text = stringResource(R.string.button_cancel), text = stringResource(R.string.button_cancel),
onClick = { pendingDeleteId = null }, onClick = {
haptic.contextClick()
pendingDeleteId = null
},
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
TextButton( TextButton(
text = stringResource(R.string.button_delete), text = stringResource(R.string.button_delete),
onClick = { onClick = {
haptic.confirm()
PasswordRepository.delete(target.id) PasswordRepository.delete(target.id)
pendingDeleteId = null pendingDeleteId = null
}, },
@@ -467,6 +448,8 @@ private fun LockscreenPasswordPage(
onDelete: (PasswordEntry) -> Unit, onDelete: (PasswordEntry) -> Unit,
onMove: (Int, Int) -> Unit, onMove: (Int, Int) -> Unit,
) { ) {
val haptic = LocalHapticFeedback.current
LazyColumn( LazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
@@ -478,7 +461,7 @@ private fun LockscreenPasswordPage(
title = stringResource(R.string.password_require_auth), title = stringResource(R.string.password_require_auth),
summary = stringResource( summary = stringResource(
if (canAuthenticate) R.string.password_require_auth_detail if (canAuthenticate) R.string.password_require_auth_detail
else R.string.password_no_auth_capability else R.string.password_no_auth_capability,
), ),
checked = requireAuth, checked = requireAuth,
enabled = canAuthenticate || requireAuth, enabled = canAuthenticate || requireAuth,
@@ -494,7 +477,10 @@ private fun LockscreenPasswordPage(
ArrowPreference( ArrowPreference(
title = stringResource(R.string.password_create_new), title = stringResource(R.string.password_create_new),
summary = stringResource(R.string.password_or_menu_hint), summary = stringResource(R.string.password_or_menu_hint),
onClick = onCreate, onClick = {
haptic.contextClick()
onCreate()
},
) )
} }
} }
@@ -521,17 +507,26 @@ private fun LockscreenPasswordPage(
PasswordCreatedState.UnauthenticatedCreated -> textUnauthenticated PasswordCreatedState.UnauthenticatedCreated -> textUnauthenticated
PasswordCreatedState.AuthenticatedCreatedModified -> textBurned PasswordCreatedState.AuthenticatedCreatedModified -> textBurned
}, },
onClick = { if (entry.cipherText != null) onRename(entry) }, onClick = {
haptic.contextClick()
if (entry.cipherText != null) onRename(entry)
},
endActions = listOf( endActions = listOf(
ReorderableList.EndAction.Icon( ReorderableList.EndAction.Icon(
icon = Icons.Rounded.Edit, icon = Icons.Rounded.Edit,
contentDescription = textEdit, contentDescription = textEdit,
onClick = { if (entry.cipherText != null) onRename(entry) }, onClick = {
haptic.contextClick()
if (entry.cipherText != null) onRename(entry)
},
), ),
ReorderableList.EndAction.Icon( ReorderableList.EndAction.Icon(
icon = Icons.Rounded.DeleteOutline, icon = Icons.Rounded.DeleteOutline,
contentDescription = textConfirm, contentDescription = textConfirm,
onClick = { onDelete(entry) }, onClick = {
haptic.contextClick()
onDelete(entry)
},
), ),
), ),
) )
@@ -563,6 +558,8 @@ private fun PasswordEditorSheet(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: (String, String) -> Unit, onConfirm: (String, String) -> Unit,
) { ) {
val haptic = LocalHapticFeedback.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
var nameBuffer by rememberSaveable(mode, show, initialName) { mutableStateOf(initialName) } var nameBuffer by rememberSaveable(mode, show, initialName) { mutableStateOf(initialName) }
var passwordBuffer by rememberSaveable(mode, show) { mutableStateOf("") } var passwordBuffer by rememberSaveable(mode, show) { mutableStateOf("") }
@@ -571,12 +568,17 @@ private fun PasswordEditorSheet(
show = show, show = show,
title = stringResource( title = stringResource(
if (mode == PasswordDialogMode.Create) R.string.password_create_new if (mode == PasswordDialogMode.Create) R.string.password_create_new
else R.string.password_rename else R.string.password_rename,
), ),
defaultWindowInsetsPadding = false, defaultWindowInsetsPadding = false,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
startAction = { startAction = {
IconButton(onClick = onDismissRequest) { IconButton(
onClick = {
haptic.contextClick()
onDismissRequest()
},
) {
Icon( Icon(
imageVector = MiuixIcons.Close, imageVector = MiuixIcons.Close,
contentDescription = stringResource(R.string.cd_close), contentDescription = stringResource(R.string.cd_close),
@@ -584,7 +586,12 @@ private fun PasswordEditorSheet(
} }
}, },
endAction = { endAction = {
IconButton(onClick = { onConfirm(nameBuffer, passwordBuffer) }) { IconButton(
onClick = {
haptic.contextClick()
onConfirm(nameBuffer, passwordBuffer)
},
) {
Icon( Icon(
imageVector = MiuixIcons.Ok, imageVector = MiuixIcons.Ok,
contentDescription = stringResource(R.string.cd_save), contentDescription = stringResource(R.string.cd_save),

View File

@@ -124,6 +124,7 @@ private fun AboutContent(
onLogoHeightChanged: (Int) -> Unit, onLogoHeightChanged: (Int) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val updateState by AppUpdateChecker.state.collectAsState() val updateState by AppUpdateChecker.state.collectAsState()
val backdrop = rememberLayerBackdrop() val backdrop = rememberLayerBackdrop()
var isOs3Effect by remember { mutableStateOf(true) } var isOs3Effect by remember { mutableStateOf(true) }
@@ -363,6 +364,7 @@ private fun AboutContent(
) )
}, },
onClick = { onClick = {
haptic.contextClick()
context.startActivity( context.startActivity(
Intent(Intent.ACTION_VIEW, AppUpdateChecker.REPO_URL.toUri()), Intent(Intent.ACTION_VIEW, AppUpdateChecker.REPO_URL.toUri()),
) )
@@ -380,6 +382,7 @@ private fun AboutContent(
} }
}, },
onClick = { onClick = {
haptic.contextClick()
context.startActivity( context.startActivity(
Intent( Intent(
Intent.ACTION_VIEW, Intent.ACTION_VIEW,
@@ -400,6 +403,7 @@ private fun AboutContent(
) )
}, },
onClick = { onClick = {
haptic.contextClick()
context.startActivity( context.startActivity(
Intent( Intent(
Intent.ACTION_VIEW, Intent.ACTION_VIEW,
@@ -424,6 +428,7 @@ private fun AboutContent(
) )
}, },
onClick = { onClick = {
haptic.contextClick()
context.startActivity( context.startActivity(
Intent( Intent(
Intent.ACTION_VIEW, Intent.ACTION_VIEW,

View File

@@ -1,36 +1,15 @@
package io.github.miuzarte.scrcpyforandroid.pages package io.github.miuzarte.scrcpyforandroid.pages
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.SwapHoriz import androidx.compose.material.icons.rounded.SwapHoriz
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.Color import androidx.compose.ui.graphics.Color
@@ -52,39 +31,16 @@ import io.github.miuzarte.scrcpyforandroid.password.PasswordPickerPopupContent
import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
import io.github.miuzarte.scrcpyforandroid.scaffolds.SectionSmallTitle import io.github.miuzarte.scrcpyforandroid.scaffolds.SectionSmallTitle
import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime import io.github.miuzarte.scrcpyforandroid.services.*
import io.github.miuzarte.scrcpyforandroid.services.ConnectionController
import io.github.miuzarte.scrcpyforandroid.services.ConnectionStateStore
import io.github.miuzarte.scrcpyforandroid.services.DeviceAdbAutoReconnectManager
import io.github.miuzarte.scrcpyforandroid.services.DeviceAdbConnectionCoordinator
import io.github.miuzarte.scrcpyforandroid.services.EventLogger
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.contextClick import io.github.miuzarte.scrcpyforandroid.ui.contextClick
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import io.github.miuzarte.scrcpyforandroid.widgets.AppListBottomSheet import io.github.miuzarte.scrcpyforandroid.widgets.*
import io.github.miuzarte.scrcpyforandroid.widgets.AppListEntry
import io.github.miuzarte.scrcpyforandroid.widgets.ConfigPanel
import io.github.miuzarte.scrcpyforandroid.widgets.DeviceTileList
import io.github.miuzarte.scrcpyforandroid.widgets.PairingCard
import io.github.miuzarte.scrcpyforandroid.widgets.PreviewCard
import io.github.miuzarte.scrcpyforandroid.widgets.QuickConnectCard
import io.github.miuzarte.scrcpyforandroid.widgets.StatusCard
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonCard
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.*
import top.yukonga.miuix.kmp.basic.DropdownEntry
import top.yukonga.miuix.kmp.basic.DropdownItem
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.blur.layerBackdrop
import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.More import top.yukonga.miuix.kmp.icon.extended.More
@@ -114,6 +70,7 @@ internal fun DeviceTabScreen(
val viewModel: DeviceTabViewModel = viewModel(factory = viewModelFactory) val viewModel: DeviceTabViewModel = viewModel(factory = viewModelFactory)
val navigator = LocalRootNavigator.current val navigator = LocalRootNavigator.current
val haptic = LocalHapticFeedback.current
var useCompactTopAppBar by remember { mutableStateOf(false) } var useCompactTopAppBar by remember { mutableStateOf(false) }
var showTwoPaneSideAction by remember { mutableStateOf(false) } var showTwoPaneSideAction by remember { mutableStateOf(false) }
var configPanelOnLeft by remember { mutableStateOf(true) } var configPanelOnLeft by remember { mutableStateOf(true) }
@@ -130,13 +87,16 @@ internal fun DeviceTabScreen(
val topAppBarActions: @Composable RowScope.() -> Unit = { val topAppBarActions: @Composable RowScope.() -> Unit = {
if (showTwoPaneSideAction) { if (showTwoPaneSideAction) {
IconButton( IconButton(
onClick = { twoPaneSideToggleRequest++ }, onClick = {
haptic.contextClick()
twoPaneSideToggleRequest++
},
) { ) {
Icon( Icon(
imageVector = Icons.Rounded.SwapHoriz, imageVector = Icons.Rounded.SwapHoriz,
contentDescription = stringResource( contentDescription = stringResource(
if (configPanelOnLeft) R.string.device_cd_config_right if (configPanelOnLeft) R.string.device_cd_config_right
else R.string.device_cd_config_left else R.string.device_cd_config_left,
), ),
) )
} }
@@ -162,9 +122,9 @@ internal fun DeviceTabScreen(
onClick = { onClick = {
EventLogger.clearLogs() EventLogger.clearLogs()
}, },
) ),
) ),
) ),
) { ) {
Icon( Icon(
imageVector = MiuixIcons.More, imageVector = MiuixIcons.More,
@@ -175,13 +135,13 @@ internal fun DeviceTabScreen(
if (useCompactTopAppBar) SmallTopAppBar( if (useCompactTopAppBar) SmallTopAppBar(
title = stringResource(R.string.device_title), title = stringResource(R.string.device_title),
color = topAppBarColor, color = topAppBarColor,
actions = topAppBarActions actions = topAppBarActions,
) )
else TopAppBar( else TopAppBar(
title = stringResource(R.string.device_title), title = stringResource(R.string.device_title),
color = topAppBarColor, color = topAppBarColor,
actions = topAppBarActions, actions = topAppBarActions,
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior,
) )
} }
}, },
@@ -286,7 +246,7 @@ internal fun DeviceTabPage(
DisposableEffect(lifecycleOwner) { DisposableEffect(lifecycleOwner) {
viewModel.setAppInForeground( viewModel.setAppInForeground(
lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED),
) )
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
when (event) { when (event) {
@@ -378,7 +338,7 @@ internal fun DeviceTabPage(
} else { } else {
viewModel.setEditingDeviceId( viewModel.setEditingDeviceId(
if (editingDeviceId != device.id) device.id if (editingDeviceId != device.id) device.id
else null else null,
) )
} }
}, },
@@ -476,7 +436,6 @@ internal fun DeviceTabPage(
} }
}, },
onOpenAdvanced = { navigator.push(RootScreen.Advanced) }, onOpenAdvanced = { navigator.push(RootScreen.Advanced) },
onStartStopHaptic = haptic::contextClick,
onStart = viewModel::startScrcpy, onStart = viewModel::startScrcpy,
onStop = viewModel::stopScrcpy, onStop = viewModel::stopScrcpy,
sessionInfo = sessionInfo, sessionInfo = sessionInfo,
@@ -588,7 +547,6 @@ internal fun DeviceTabPage(
} }
}, },
onOpenAdvanced = { navigator.push(RootScreen.Advanced) }, onOpenAdvanced = { navigator.push(RootScreen.Advanced) },
onStartStopHaptic = haptic::contextClick,
onStart = viewModel::startScrcpy, onStart = viewModel::startScrcpy,
onStop = viewModel::stopScrcpy, onStop = viewModel::stopScrcpy,
sessionInfo = sessionInfo, sessionInfo = sessionInfo,
@@ -715,7 +673,7 @@ internal fun DeviceTabPage(
.padding(contentPadding) .padding(contentPadding)
.padding( .padding(
horizontal = UiSpacing.PageHorizontal, horizontal = UiSpacing.PageHorizontal,
vertical = UiSpacing.PageVertical vertical = UiSpacing.PageVertical,
), ),
horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem), horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem),
) { ) {
@@ -791,6 +749,7 @@ internal fun DeviceTabPage(
summary = if (app?.label != null) task.packageName else null, summary = if (app?.label != null) task.packageName else null,
system = app?.system, system = app?.system,
onClick = { onClick = {
haptic.contextClick()
viewModel.hideRecentTasks() viewModel.hideRecentTasks()
if (sessionInfo == null) viewModel.startScrcpy(task.packageName) if (sessionInfo == null) viewModel.startScrcpy(task.packageName)
else viewModel.launchAppWithFallback(task.packageName) else viewModel.launchAppWithFallback(task.packageName)

View File

@@ -3,42 +3,22 @@ package io.github.miuzarte.scrcpyforandroid.pages
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.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
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
import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile
import androidx.compose.material.icons.rounded.Download import androidx.compose.material.icons.rounded.*
import androidx.compose.material.icons.rounded.Folder import androidx.compose.runtime.*
import androidx.compose.material.icons.rounded.Image
import androidx.compose.material.icons.rounded.Link
import androidx.compose.material.icons.rounded.RawOff
import androidx.compose.material.icons.rounded.RawOn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.Color 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.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -50,28 +30,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import io.github.miuzarte.scrcpyforandroid.R import io.github.miuzarte.scrcpyforandroid.R
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.services.AppRuntime import io.github.miuzarte.scrcpyforandroid.services.*
import io.github.miuzarte.scrcpyforandroid.services.DirectoryDownloadSnapshot import io.github.miuzarte.scrcpyforandroid.ui.*
import io.github.miuzarte.scrcpyforandroid.services.FileManagerService import top.yukonga.miuix.kmp.basic.*
import io.github.miuzarte.scrcpyforandroid.services.RemoteFileEntry
import io.github.miuzarte.scrcpyforandroid.services.RemoteFileKind
import io.github.miuzarte.scrcpyforandroid.services.RemoteFileStat
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.DropdownEntry
import top.yukonga.miuix.kmp.basic.DropdownItem
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.PullToRefresh
import top.yukonga.miuix.kmp.basic.PullToRefreshState
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.rememberPullToRefreshState
import top.yukonga.miuix.kmp.blur.layerBackdrop 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.More import top.yukonga.miuix.kmp.icon.extended.More
@@ -90,6 +51,7 @@ fun FileManagerScreen(
onNavigateUpActionChange: (((() -> Boolean)?) -> Unit)? = null, onNavigateUpActionChange: (((() -> Boolean)?) -> Unit)? = null,
) { ) {
val viewModel: FileManagerViewModel = viewModel() val viewModel: FileManagerViewModel = viewModel()
val haptic = LocalHapticFeedback.current
val context = LocalContext.current val context = LocalContext.current
val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current) val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current)
val blurActive = blurBackdrop != null val blurActive = blurBackdrop != null
@@ -178,7 +140,10 @@ fun FileManagerScreen(
else colorScheme.surface, else colorScheme.surface,
navigationIcon = { navigationIcon = {
IconButton( IconButton(
onClick = viewModel::navigateUp, onClick = {
haptic.contextClick()
viewModel.navigateUp()
},
enabled = pathStack.size > 1, enabled = pathStack.size > 1,
) { ) {
Icon( Icon(
@@ -194,10 +159,12 @@ fun FileManagerScreen(
.fillMaxWidth() .fillMaxWidth()
.combinedClickable( .combinedClickable(
onClick = { onClick = {
haptic.contextClick()
pathInput = currentPath pathInput = currentPath
showPathDialog = true showPathDialog = true
}, },
onLongClick = { onLongClick = {
// 自带 haptic
pathInput = currentPath pathInput = currentPath
showPathDialog = true showPathDialog = true
}, },
@@ -244,11 +211,11 @@ fun FileManagerScreen(
2 -> FileManagerSortField.TIME 2 -> FileManagerSortField.TIME
3 -> FileManagerSortField.EXTENSION 3 -> FileManagerSortField.EXTENSION
else -> FileManagerSortField.NAME else -> FileManagerSortField.NAME
} },
) )
}, },
) )
} },
), ),
DropdownEntry( DropdownEntry(
items = dirOptions.mapIndexed { i, option -> items = dirOptions.mapIndexed { i, option ->
@@ -257,13 +224,13 @@ fun FileManagerScreen(
selected = i == dirIdx, selected = i == dirIdx,
onClick = { onClick = {
viewModel.updateSort( viewModel.updateSort(
descending = i == 1 descending = i == 1,
) )
}, },
) )
} },
), ),
) ),
) { ) {
Icon( Icon(
imageVector = MiuixIcons.Tune, imageVector = MiuixIcons.Tune,
@@ -286,9 +253,9 @@ fun FileManagerScreen(
onClick = { onClick = {
uploadLauncher.launch(arrayOf("*/*")) uploadLauncher.launch(arrayOf("*/*"))
}, },
) ),
) ),
) ),
) { ) {
Icon( Icon(
imageVector = MiuixIcons.More, imageVector = MiuixIcons.More,
@@ -303,7 +270,7 @@ fun FileManagerScreen(
Box( Box(
modifier = modifier =
if (blurActive) Modifier.layerBackdrop(blurBackdrop) if (blurActive) Modifier.layerBackdrop(blurBackdrop)
else Modifier else Modifier,
) { ) {
FileManagerPage( FileManagerPage(
contentPadding = pagePadding, contentPadding = pagePadding,
@@ -320,7 +287,7 @@ fun FileManagerScreen(
viewModel.saveScrollPosition( viewModel.saveScrollPosition(
currentPath, currentPath,
listState.firstVisibleItemIndex, listState.firstVisibleItemIndex,
listState.firstVisibleItemScrollOffset listState.firstVisibleItemScrollOffset,
) )
viewModel.openEntry(entry) viewModel.openEntry(entry)
}, },
@@ -356,30 +323,28 @@ fun FileManagerScreen(
&& (!entry.isDirectory || selectedSnapshot != null), && (!entry.isDirectory || selectedSnapshot != null),
) )
} }
if (showPathDialog) {
PathJumpDialog( PathJumpDialog(
show = showPathDialog, show = showPathDialog,
path = pathInput, path = pathInput,
onPathChange = { pathInput = it }, onPathChange = { pathInput = it },
onDismissRequest = { showPathDialog = false }, onDismissRequest = { showPathDialog = false },
onConfirm = { onConfirm = {
showPathDialog = false showPathDialog = false
viewModel.jumpToPath(pathInput) viewModel.jumpToPath(pathInput)
}, },
) )
}
if (showCreateFolderDialog) { CreateFolderDialog(
CreateFolderDialog( show = showCreateFolderDialog,
show = showCreateFolderDialog, folderName = newFolderName,
folderName = newFolderName, onFolderNameChange = { newFolderName = it },
onFolderNameChange = { newFolderName = it }, onDismissRequest = { showCreateFolderDialog = false },
onDismissRequest = { showCreateFolderDialog = false }, onConfirm = {
onConfirm = { showCreateFolderDialog = false
showCreateFolderDialog = false viewModel.createFolder(newFolderName)
viewModel.createFolder(newFolderName) },
}, )
)
}
} }
@Composable @Composable
@@ -430,17 +395,17 @@ private fun FileManagerPage(
when { when {
loading -> FileManagerStatusCard( loading -> FileManagerStatusCard(
message = stringResource(R.string.text_loading), message = stringResource(R.string.text_loading),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
errorText != null -> FileManagerStatusCard( errorText != null -> FileManagerStatusCard(
message = stringResource(R.string.fm_load_failed, errorText), message = stringResource(R.string.fm_load_failed, errorText),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
displayedEntries.isEmpty() -> FileManagerStatusCard( displayedEntries.isEmpty() -> FileManagerStatusCard(
message = stringResource(R.string.fm_empty_dir), message = stringResource(R.string.fm_empty_dir),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
} }
} }
@@ -457,7 +422,7 @@ private fun FileManagerPage(
items(fileRows) { rowEntries -> items(fileRows) { rowEntries ->
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem) horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem),
) { ) {
rowEntries.forEach { entry -> rowEntries.forEach { entry ->
FileManagerItemCard( FileManagerItemCard(
@@ -503,10 +468,15 @@ private fun FileManagerItemCard(
onLongClick: () -> Unit, onLongClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val haptic = LocalHapticFeedback.current
Card( Card(
modifier = modifier modifier = modifier
.combinedClickable( .combinedClickable(
onClick = onClick, onClick = {
haptic.contextClick()
onClick()
},
onLongClick = onLongClick, onLongClick = onLongClick,
), ),
) { ) {
@@ -560,6 +530,8 @@ private fun FileDetailsBottomSheet(
onDownload: () -> Unit, onDownload: () -> Unit,
downloadEnabled: Boolean, downloadEnabled: Boolean,
) { ) {
val haptic = LocalHapticFeedback.current
OverlayBottomSheet( OverlayBottomSheet(
show = show, show = show,
title = stringResource(R.string.fm_file_details), title = stringResource(R.string.fm_file_details),
@@ -567,7 +539,10 @@ private fun FileDetailsBottomSheet(
onDismissFinished = onDismissFinished, onDismissFinished = onDismissFinished,
startAction = { startAction = {
IconButton( IconButton(
onClick = onToggleRaw onClick = {
haptic.contextClick()
onToggleRaw()
},
) { ) {
Icon( Icon(
imageVector = imageVector =
@@ -575,14 +550,17 @@ private fun FileDetailsBottomSheet(
else Icons.Rounded.RawOn, else Icons.Rounded.RawOn,
contentDescription = stringResource( contentDescription = stringResource(
if (!showingRaw) R.string.fm_show_raw if (!showingRaw) R.string.fm_show_raw
else R.string.fm_show_parsed else R.string.fm_show_parsed,
), ),
) )
} }
}, },
endAction = { endAction = {
IconButton( IconButton(
onClick = onDownload, onClick = {
haptic.contextClick()
onDownload()
},
enabled = downloadEnabled, enabled = downloadEnabled,
) { ) {
Icon( Icon(
@@ -595,7 +573,7 @@ private fun FileDetailsBottomSheet(
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight(2f / 3f) .fillMaxHeight(2f / 3f),
) { ) {
item { item {
TextField( TextField(
@@ -618,6 +596,8 @@ private fun PathJumpDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: () -> Unit, onConfirm: () -> Unit,
) { ) {
val haptic = LocalHapticFeedback.current
OverlayDialog( OverlayDialog(
show = show, show = show,
title = stringResource(R.string.fm_goto_path), title = stringResource(R.string.fm_goto_path),
@@ -628,17 +608,26 @@ private fun PathJumpDialog(
TextField( TextField(
value = path, value = path,
onValueChange = onPathChange, onValueChange = onPathChange,
label = "/storage/emulated/0", // label = "/storage/emulated/0",
useLabelAsPlaceholder = true, // useLabelAsPlaceholder = true,
) )
Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem)) { Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem),
) {
TextButton( TextButton(
text = stringResource(R.string.button_cancel), text = stringResource(R.string.button_cancel),
onClick = onDismissRequest, onClick = {
haptic.contextClick()
onDismissRequest()
},
) )
TextButton( TextButton(
text = stringResource(R.string.button_confirm), text = stringResource(R.string.button_confirm),
onClick = onConfirm, onClick = {
haptic.confirm()
onConfirm()
},
) )
} }
} }
@@ -653,6 +642,8 @@ private fun CreateFolderDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: () -> Unit, onConfirm: () -> Unit,
) { ) {
val haptic = LocalHapticFeedback.current
OverlayDialog( OverlayDialog(
show = show, show = show,
title = stringResource(R.string.fm_title_create_folder), title = stringResource(R.string.fm_title_create_folder),
@@ -666,14 +657,23 @@ private fun CreateFolderDialog(
label = stringResource(R.string.fm_label_new_folder), label = stringResource(R.string.fm_label_new_folder),
useLabelAsPlaceholder = true, useLabelAsPlaceholder = true,
) )
Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem)) { Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem),
) {
TextButton( TextButton(
text = stringResource(R.string.button_cancel), text = stringResource(R.string.button_cancel),
onClick = onDismissRequest, onClick = {
haptic.contextClick()
onDismissRequest()
},
) )
TextButton( TextButton(
text = stringResource(R.string.fm_button_create), text = stringResource(R.string.fm_button_create),
onClick = onConfirm, onClick = {
haptic.confirm()
onConfirm()
},
) )
} }
} }
@@ -695,13 +695,13 @@ private fun buildDetailsText(
): String { ): String {
val details = StringBuilder( val details = StringBuilder(
if (showRaw) stat.rawOutput if (showRaw) stat.rawOutput
else FileManagerService.formatStatDetails(stat, directorySnapshot) else FileManagerService.formatStatDetails(stat, directorySnapshot),
) )
if (targetStat != null) { if (targetStat != null) {
details.append("\n\n${AppRuntime.stringResource(R.string.fm_stat_target_info)}\n") details.append("\n\n${AppRuntime.stringResource(R.string.fm_stat_target_info)}\n")
details.append( details.append(
if (showRaw) targetStat.rawOutput if (showRaw) targetStat.rawOutput
else FileManagerService.formatStatDetails(targetStat) else FileManagerService.formatStatDetails(targetStat),
) )
} }
return details.toString() return details.toString()

View File

@@ -1,17 +1,7 @@
package io.github.miuzarte.scrcpyforandroid.pages package io.github.miuzarte.scrcpyforandroid.pages
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@@ -21,23 +11,13 @@ import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Android import androidx.compose.material.icons.rounded.Android
import androidx.compose.material.icons.rounded.DeleteOutline import androidx.compose.material.icons.rounded.DeleteOutline
import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Edit
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
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.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -51,20 +31,10 @@ import io.github.miuzarte.scrcpyforandroid.miuix.SpinnerEntry
import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts import io.github.miuzarte.scrcpyforandroid.models.DeviceShortcuts
import io.github.miuzarte.scrcpyforandroid.models.ScrcpyOptions.Crop import io.github.miuzarte.scrcpyforandroid.models.ScrcpyOptions.Crop
import io.github.miuzarte.scrcpyforandroid.models.ScrcpyOptions.NewDisplay import io.github.miuzarte.scrcpyforandroid.models.ScrcpyOptions.NewDisplay
import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.*
import io.github.miuzarte.scrcpyforandroid.scaffolds.OverlaySpinnerWithFallback
import io.github.miuzarte.scrcpyforandroid.scaffolds.ReorderableList
import io.github.miuzarte.scrcpyforandroid.scaffolds.ArrowSlider
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField
import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.*
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.CameraFacing
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Codec
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.DisplayImePolicy
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.LogLevel
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Tick
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyProfiles import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyProfiles
@@ -72,30 +42,10 @@ 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.*
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import io.github.miuzarte.scrcpyforandroid.widgets.RecordPreferences import io.github.miuzarte.scrcpyforandroid.widgets.RecordPreferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers import top.yukonga.miuix.kmp.basic.*
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.ButtonDefaults
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.DropdownEntry
import top.yukonga.miuix.kmp.basic.DropdownItem
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.SnackbarHost
import top.yukonga.miuix.kmp.basic.TabRow
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.blur.layerBackdrop
import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.More import top.yukonga.miuix.kmp.icon.extended.More
@@ -142,7 +92,7 @@ internal fun ScrcpyAllOptionsScreen(
soBundleShared soBundleShared
else scrcpyProfilesState.profiles else scrcpyProfilesState.profiles
.firstOrNull { it.id == selectedProfileId } .firstOrNull { it.id == selectedProfileId }
?.bundle ?: soBundleShared ?.bundle ?: soBundleShared,
) )
} }
val lastValidSoBundleState = rememberSaveable(selectedProfileId) { val lastValidSoBundleState = rememberSaveable(selectedProfileId) {
@@ -186,7 +136,7 @@ internal fun ScrcpyAllOptionsScreen(
if (device.scrcpyProfileId == profileId) if (device.scrcpyProfileId == profileId)
device.copy(scrcpyProfileId = ScrcpyOptions.GLOBAL_PROFILE_ID) device.copy(scrcpyProfileId = ScrcpyOptions.GLOBAL_PROFILE_ID)
else device else device
} },
) )
if (updated != shortcuts) { if (updated != shortcuts) {
quickDevices.updateBundle { bundle -> quickDevices.updateBundle { bundle ->
@@ -235,9 +185,9 @@ internal fun ScrcpyAllOptionsScreen(
onClick = { onClick = {
showManageProfilesSheet = true showManageProfilesSheet = true
}, },
) ),
) ),
) ),
) { ) {
Icon( Icon(
imageVector = MiuixIcons.More, imageVector = MiuixIcons.More,
@@ -277,7 +227,7 @@ internal fun ScrcpyAllOptionsScreen(
height = 48.dp, height = 48.dp,
itemSpacing = UiSpacing.Medium, itemSpacing = UiSpacing.Medium,
) )
} },
) )
} }
}, },
@@ -286,7 +236,7 @@ internal fun ScrcpyAllOptionsScreen(
Box( Box(
modifier = modifier =
if (blurActive) Modifier.layerBackdrop(blurBackdrop) if (blurActive) Modifier.layerBackdrop(blurBackdrop)
else Modifier else Modifier,
) { ) {
ScrcpyAllOptionsPage( ScrcpyAllOptionsPage(
contentPadding = contentPadding, contentPadding = contentPadding,
@@ -606,7 +556,7 @@ internal fun ScrcpyAllOptionsPage(
val screenOffTimeoutPresetIndex = rememberSaveable(soBundle.screenOffTimeout) { val screenOffTimeoutPresetIndex = rememberSaveable(soBundle.screenOffTimeout) {
ScrcpyPresets.ScreenOffTimeout.indexOfOrNearest( ScrcpyPresets.ScreenOffTimeout.indexOfOrNearest(
Tick(soBundle.screenOffTimeout).sec.toInt().coerceAtLeast(0) Tick(soBundle.screenOffTimeout).sec.toInt().coerceAtLeast(0),
) )
} }
@@ -671,7 +621,7 @@ internal fun ScrcpyAllOptionsPage(
SpinnerEntry( SpinnerEntry(
title = info.id, title = info.id,
summary = info.type.s, summary = info.type.s,
) ),
) )
} }
} }
@@ -702,7 +652,7 @@ internal fun ScrcpyAllOptionsPage(
SpinnerEntry( SpinnerEntry(
title = info.id, title = info.id,
summary = info.type.s, summary = info.type.s,
) ),
) )
} }
} }
@@ -760,7 +710,7 @@ internal fun ScrcpyAllOptionsPage(
}, },
title = app.label?.takeIf { it.isNotBlank() } ?: app.packageName, title = app.label?.takeIf { it.isNotBlank() } ?: app.packageName,
summary = app.packageName, summary = app.packageName,
) ),
) )
} }
} }
@@ -893,13 +843,13 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.turnScreenOff, checked = soBundle.turnScreenOff,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
turnScreenOff = it turnScreenOff = it,
) )
if (it) AppRuntime.snackbar( if (it) AppRuntime.snackbar(
// github.com/Genymobile/scrcpy/issues/3376 // github.com/Genymobile/scrcpy/issues/3376
// github.com/Genymobile/scrcpy/issues/4587 // github.com/Genymobile/scrcpy/issues/4587
// github.com/Genymobile/scrcpy/issues/5676 // github.com/Genymobile/scrcpy/issues/5676
R.string.scrcpyopt_turn_screen_off_note R.string.scrcpyopt_turn_screen_off_note,
) )
}, },
) )
@@ -909,7 +859,7 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.control, checked = !soBundle.control,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
control = !it control = !it,
) )
}, },
) )
@@ -919,7 +869,7 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.video, checked = !soBundle.video,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
video = !it video = !it,
) )
}, },
) )
@@ -929,7 +879,7 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.videoPlayback, checked = !soBundle.videoPlayback,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoPlayback = !it videoPlayback = !it,
) )
}, },
enabled = soBundle.video, enabled = soBundle.video,
@@ -940,7 +890,7 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.audio, checked = !soBundle.audio,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
audio = !it audio = !it,
) )
}, },
) )
@@ -950,7 +900,7 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.audioPlayback, checked = !soBundle.audioPlayback,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioPlayback = !it audioPlayback = !it,
) )
}, },
enabled = soBundle.audio, enabled = soBundle.audio,
@@ -968,7 +918,7 @@ internal fun ScrcpyAllOptionsPage(
?.toLong() ?.toLong()
?.let(Tick::fromSec) ?.let(Tick::fromSec)
?.value ?.value
?: -1 ?: -1,
) )
}, },
valueRange = 0f..ScrcpyPresets.ScreenOffTimeout.lastIndex.toFloat(), valueRange = 0f..ScrcpyPresets.ScreenOffTimeout.lastIndex.toFloat(),
@@ -989,7 +939,7 @@ internal fun ScrcpyAllOptionsPage(
?.takeIf { value -> value > 0 } ?.takeIf { value -> value > 0 }
?.let(Tick::fromSec) ?.let(Tick::fromSec)
?.value ?.value
?: -1 ?: -1,
) )
}, },
) )
@@ -999,7 +949,7 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.powerOn, checked = !soBundle.powerOn,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
powerOn = !it powerOn = !it,
) )
}, },
) )
@@ -1009,7 +959,7 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.powerOffOnClose, checked = soBundle.powerOffOnClose,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
powerOffOnClose = it powerOffOnClose = it,
) )
}, },
) )
@@ -1019,7 +969,7 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.stayAwake, checked = soBundle.stayAwake,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
stayAwake = it stayAwake = it,
) )
}, },
) )
@@ -1029,7 +979,7 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.keepActive, checked = soBundle.keepActive,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
keepActive = it keepActive = it,
) )
}, },
) )
@@ -1039,7 +989,7 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.showTouches, checked = soBundle.showTouches,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
showTouches = it showTouches = it,
) )
}, },
) )
@@ -1049,7 +999,7 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.fullscreen, checked = soBundle.fullscreen,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
fullscreen = it fullscreen = it,
) )
}, },
) )
@@ -1059,10 +1009,10 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.disableScreensaver, checked = soBundle.disableScreensaver,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
disableScreensaver = it disableScreensaver = it,
) )
if (it) AppRuntime.snackbar( if (it) AppRuntime.snackbar(
R.string.scrcpyopt_disable_screensaver_note R.string.scrcpyopt_disable_screensaver_note,
) )
}, },
) )
@@ -1081,7 +1031,7 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.killAdbOnClose, checked = soBundle.killAdbOnClose,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
killAdbOnClose = it killAdbOnClose = it,
) )
}, },
) )
@@ -1097,7 +1047,7 @@ internal fun ScrcpyAllOptionsPage(
selectedIndex = audioCodecIndex, selectedIndex = audioCodecIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioCodec = Codec.AUDIO[it].string audioCodec = Codec.AUDIO[it].string,
) )
}, },
) )
@@ -1111,7 +1061,7 @@ internal fun ScrcpyAllOptionsPage(
val idx = value.roundToInt() val idx = value.roundToInt()
.coerceIn(0, ScrcpyPresets.AudioBitRate.lastIndex) .coerceIn(0, ScrcpyPresets.AudioBitRate.lastIndex)
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000 audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000,
) )
}, },
valueRange = 0f..ScrcpyPresets.AudioBitRate.lastIndex.toFloat(), valueRange = 0f..ScrcpyPresets.AudioBitRate.lastIndex.toFloat(),
@@ -1129,7 +1079,7 @@ internal fun ScrcpyAllOptionsPage(
?.takeIf { it >= 0 } ?.takeIf { it >= 0 }
?.let { ?.let {
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioBitRate = it * 1000 audioBitRate = it * 1000,
) )
} }
}, },
@@ -1142,7 +1092,7 @@ internal fun ScrcpyAllOptionsPage(
selectedIndex = videoCodecIndex, selectedIndex = videoCodecIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoCodec = Codec.VIDEO[it].string videoCodec = Codec.VIDEO[it].string,
) )
}, },
) )
@@ -1152,7 +1102,7 @@ internal fun ScrcpyAllOptionsPage(
value = soBundle.videoBitRate / 1_000_000f, value = soBundle.videoBitRate / 1_000_000f,
onValueChange = { mbps -> onValueChange = { mbps ->
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoBitRate = (mbps * 10).roundToInt() * (1_000_000 / 10) videoBitRate = (mbps * 10).roundToInt() * (1_000_000 / 10),
) )
}, },
valueRange = 0f..40f, valueRange = 0f..40f,
@@ -1182,7 +1132,7 @@ internal fun ScrcpyAllOptionsPage(
raw.toFloatOrNull()?.let { parsed -> raw.toFloatOrNull()?.let { parsed ->
if (parsed >= 0f) { if (parsed >= 0f) {
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoBitRate = (parsed * 1_000_000f).roundToInt() videoBitRate = (parsed * 1_000_000f).roundToInt(),
) )
} }
} }
@@ -1200,7 +1150,7 @@ internal fun ScrcpyAllOptionsPage(
selectedIndex = videoSourceIndex, selectedIndex = videoSourceIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoSource = VideoSource.entries[it].string videoSource = VideoSource.entries[it].string,
) )
}, },
) )
@@ -1236,7 +1186,7 @@ internal fun ScrcpyAllOptionsPage(
soBundle = soBundle.copy( soBundle = soBundle.copy(
displayId = displayId =
if (it == 0) -1 if (it == 0) -1
else displays[it - 1].id else displays[it - 1].id,
) )
}, },
) )
@@ -1248,7 +1198,7 @@ internal fun ScrcpyAllOptionsPage(
val idx = it.roundToInt() val idx = it.roundToInt()
.coerceIn(0, ScrcpyPresets.MinSizeAlignment.lastIndex) .coerceIn(0, ScrcpyPresets.MinSizeAlignment.lastIndex)
soBundle = soBundle.copy( soBundle = soBundle.copy(
minSizeAlignment = ScrcpyPresets.MinSizeAlignment[idx] minSizeAlignment = ScrcpyPresets.MinSizeAlignment[idx],
) )
}, },
valueRange = 0f..ScrcpyPresets.MinSizeAlignment.lastIndex.toFloat(), valueRange = 0f..ScrcpyPresets.MinSizeAlignment.lastIndex.toFloat(),
@@ -1269,7 +1219,7 @@ internal fun ScrcpyAllOptionsPage(
return@ArrowSlider return@ArrowSlider
} }
soBundle = soBundle.copy( soBundle = soBundle.copy(
minSizeAlignment = parsed minSizeAlignment = parsed,
) )
}, },
) )
@@ -1281,7 +1231,7 @@ internal fun ScrcpyAllOptionsPage(
val idx = it.roundToInt() val idx = it.roundToInt()
.coerceIn(0, ScrcpyPresets.MaxSize.lastIndex) .coerceIn(0, ScrcpyPresets.MaxSize.lastIndex)
soBundle = soBundle.copy( soBundle = soBundle.copy(
maxSize = ScrcpyPresets.MaxSize[idx] maxSize = ScrcpyPresets.MaxSize[idx],
) )
}, },
valueRange = 0f..ScrcpyPresets.MaxSize.lastIndex.toFloat(), valueRange = 0f..ScrcpyPresets.MaxSize.lastIndex.toFloat(),
@@ -1300,7 +1250,7 @@ internal fun ScrcpyAllOptionsPage(
inputValueRange = 0f..UInt.MAX_VALUE.toFloat(), inputValueRange = 0f..UInt.MAX_VALUE.toFloat(),
onInputConfirm = { onInputConfirm = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
maxSize = it.toIntOrNull() ?: run { 0 } maxSize = it.toIntOrNull() ?: run { 0 },
) )
}, },
) )
@@ -1314,7 +1264,7 @@ internal fun ScrcpyAllOptionsPage(
soBundle = soBundle.copy( soBundle = soBundle.copy(
maxFps = maxFps =
if (idx == 0) "" if (idx == 0) ""
else ScrcpyPresets.MaxFPS[idx].toString() else ScrcpyPresets.MaxFPS[idx].toString(),
) )
}, },
valueRange = 0f..ScrcpyPresets.MaxFPS.lastIndex.toFloat(), valueRange = 0f..ScrcpyPresets.MaxFPS.lastIndex.toFloat(),
@@ -1330,7 +1280,7 @@ internal fun ScrcpyAllOptionsPage(
inputValueRange = 0f..UShort.MAX_VALUE.toFloat(), inputValueRange = 0f..UShort.MAX_VALUE.toFloat(),
onInputConfirm = { onInputConfirm = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
maxFps = it maxFps = it,
) )
}, },
) )
@@ -1368,7 +1318,7 @@ internal fun ScrcpyAllOptionsPage(
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraId = cameraId =
if (it == 0) "" if (it == 0) ""
else cameras[it - 1].id else cameras[it - 1].id,
) )
}, },
) )
@@ -1381,7 +1331,7 @@ internal fun ScrcpyAllOptionsPage(
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraFacing = cameraFacing =
if (it == 0) "" if (it == 0) ""
else CameraFacing.entries[it].string else CameraFacing.entries[it].string,
) )
}, },
) )
@@ -1425,7 +1375,7 @@ internal fun ScrcpyAllOptionsPage(
1 -> { 1 -> {
// "自定义" - 进入自定义输入模式 // "自定义" - 进入自定义输入模式
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraSizeUseCustom = true cameraSizeUseCustom = true,
) )
cameraSizeCustomInput = "" cameraSizeCustomInput = ""
} }
@@ -1452,11 +1402,11 @@ internal fun ScrcpyAllOptionsPage(
// 输入的值存在于列表中, 取消自定义输入 // 输入的值存在于列表中, 取消自定义输入
soBundle.copy( soBundle.copy(
cameraSize = cameraSizeCustomInput, cameraSize = cameraSizeCustomInput,
cameraSizeUseCustom = false cameraSizeUseCustom = false,
) )
} else { } else {
soBundle.copy( soBundle.copy(
cameraSizeCustom = cameraSizeCustomInput cameraSizeCustom = cameraSizeCustomInput,
) )
} }
}, },
@@ -1473,7 +1423,7 @@ internal fun ScrcpyAllOptionsPage(
onValueChange = { cameraArInput = it }, onValueChange = { cameraArInput = it },
onFocusLost = { onFocusLost = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraAr = cameraArInput cameraAr = cameraArInput,
) )
}, },
label = "--camera-ar", label = "--camera-ar",
@@ -1487,7 +1437,7 @@ internal fun ScrcpyAllOptionsPage(
onValueChange = { cameraZoomInput = it }, onValueChange = { cameraZoomInput = it },
onFocusLost = { onFocusLost = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraZoom = cameraZoomInput cameraZoom = cameraZoomInput,
) )
}, },
label = "--camera-zoom", label = "--camera-zoom",
@@ -1504,7 +1454,7 @@ internal fun ScrcpyAllOptionsPage(
val idx = value.roundToInt() val idx = value.roundToInt()
.coerceIn(0, ScrcpyPresets.CameraFps.lastIndex) .coerceIn(0, ScrcpyPresets.CameraFps.lastIndex)
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraFps = ScrcpyPresets.CameraFps[idx] cameraFps = ScrcpyPresets.CameraFps[idx],
) )
}, },
valueRange = 0f..ScrcpyPresets.CameraFps.lastIndex.toFloat(), valueRange = 0f..ScrcpyPresets.CameraFps.lastIndex.toFloat(),
@@ -1521,7 +1471,7 @@ internal fun ScrcpyAllOptionsPage(
inputValueRange = 0f..UShort.MAX_VALUE.toFloat(), inputValueRange = 0f..UShort.MAX_VALUE.toFloat(),
onInputConfirm = { onInputConfirm = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraFps = it.toIntOrNull() ?: run { 0 } cameraFps = it.toIntOrNull() ?: run { 0 },
) )
}, },
) )
@@ -1531,7 +1481,7 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.cameraHighSpeed, checked = soBundle.cameraHighSpeed,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraHighSpeed = it cameraHighSpeed = it,
) )
}, },
) )
@@ -1541,7 +1491,7 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.cameraTorch, checked = soBundle.cameraTorch,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
cameraTorch = it cameraTorch = it,
) )
}, },
) )
@@ -1560,7 +1510,7 @@ internal fun ScrcpyAllOptionsPage(
selectedIndex = audioSourceIndex, selectedIndex = audioSourceIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioSource = AudioSource.entries[it].string audioSource = AudioSource.entries[it].string,
) )
}, },
) )
@@ -1570,7 +1520,7 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.audioDup, checked = soBundle.audioDup,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioDup = it audioDup = it,
) )
}, },
) )
@@ -1580,7 +1530,7 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.requireAudio, checked = soBundle.requireAudio,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
requireAudio = it requireAudio = it,
) )
}, },
) )
@@ -1619,7 +1569,7 @@ internal fun ScrcpyAllOptionsPage(
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoEncoder = videoEncoder =
if (it == 0) "" if (it == 0) ""
else videoEncoders[it - 1].id else videoEncoders[it - 1].id,
) )
}, },
) )
@@ -1628,7 +1578,7 @@ internal fun ScrcpyAllOptionsPage(
onValueChange = { videoCodecOptionsInput = it }, onValueChange = { videoCodecOptionsInput = it },
onFocusLost = { onFocusLost = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoCodecOptions = videoCodecOptionsInput videoCodecOptions = videoCodecOptionsInput,
) )
}, },
label = "--video-codec-options", label = "--video-codec-options",
@@ -1667,7 +1617,7 @@ internal fun ScrcpyAllOptionsPage(
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioEncoder = audioEncoder =
if (it == 0) "" if (it == 0) ""
else audioEncoders[it - 1].id else audioEncoders[it - 1].id,
) )
}, },
) )
@@ -1676,7 +1626,7 @@ internal fun ScrcpyAllOptionsPage(
onValueChange = { audioCodecOptionsInput = it }, onValueChange = { audioCodecOptionsInput = it },
onFocusLost = { onFocusLost = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioCodecOptions = audioCodecOptionsInput audioCodecOptions = audioCodecOptionsInput,
) )
}, },
label = "--audio-codec-options", label = "--audio-codec-options",
@@ -1699,7 +1649,7 @@ internal fun ScrcpyAllOptionsPage(
soBundle = soBundle.copy( soBundle = soBundle.copy(
displayImePolicy = displayImePolicy =
if (it == 0) "" if (it == 0) ""
else DisplayImePolicy.entries[it].string else DisplayImePolicy.entries[it].string,
) )
}, },
) )
@@ -1709,7 +1659,7 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.vdDestroyContent, checked = !soBundle.vdDestroyContent,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
vdDestroyContent = !it vdDestroyContent = !it,
) )
}, },
) )
@@ -1719,7 +1669,7 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.vdSystemDecorations, checked = !soBundle.vdSystemDecorations,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
vdSystemDecorations = !it vdSystemDecorations = !it,
) )
}, },
) )
@@ -1729,10 +1679,10 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.downsizeOnError, checked = !soBundle.downsizeOnError,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
downsizeOnError = !it downsizeOnError = !it,
) )
if (it) AppRuntime.snackbar( if (it) AppRuntime.snackbar(
R.string.scrcpyopt_no_downsize_on_error_desc R.string.scrcpyopt_no_downsize_on_error_desc,
) )
}, },
) )
@@ -1742,7 +1692,7 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.legacyPaste, checked = soBundle.legacyPaste,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
legacyPaste = it legacyPaste = it,
) )
}, },
) )
@@ -1761,7 +1711,7 @@ internal fun ScrcpyAllOptionsPage(
1 -> ClientOptions.KeyInjectMode.PREFER_TEXT.string 1 -> ClientOptions.KeyInjectMode.PREFER_TEXT.string
2 -> ClientOptions.KeyInjectMode.RAW.string 2 -> ClientOptions.KeyInjectMode.RAW.string
else -> ClientOptions.KeyInjectMode.MIXED.string else -> ClientOptions.KeyInjectMode.MIXED.string
} },
) )
}, },
) )
@@ -1771,7 +1721,7 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.forwardKeyRepeat, checked = !soBundle.forwardKeyRepeat,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
forwardKeyRepeat = !it forwardKeyRepeat = !it,
) )
}, },
enabled = soBundle.keyInjectMode != ClientOptions.KeyInjectMode.PREFER_TEXT.string, enabled = soBundle.keyInjectMode != ClientOptions.KeyInjectMode.PREFER_TEXT.string,
@@ -1782,7 +1732,7 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.clipboardAutosync, checked = !soBundle.clipboardAutosync,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
clipboardAutosync = !it clipboardAutosync = !it,
) )
}, },
) )
@@ -1792,7 +1742,7 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.mouseHover, checked = !soBundle.mouseHover,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
mouseHover = !it mouseHover = !it,
) )
}, },
) )
@@ -1802,10 +1752,10 @@ internal fun ScrcpyAllOptionsPage(
checked = !soBundle.cleanup, checked = !soBundle.cleanup,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
cleanup = !it cleanup = !it,
) )
if (it) AppRuntime.snackbar( if (it) AppRuntime.snackbar(
R.string.scrcpyopt_no_cleanup_desc R.string.scrcpyopt_no_cleanup_desc,
) )
}, },
) )
@@ -1815,10 +1765,10 @@ internal fun ScrcpyAllOptionsPage(
checked = soBundle.flexDisplay, checked = soBundle.flexDisplay,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
flexDisplay = it flexDisplay = it,
) )
if (it) AppRuntime.snackbar( if (it) AppRuntime.snackbar(
"untested" "untested",
) )
}, },
) )
@@ -1865,7 +1815,7 @@ internal fun ScrcpyAllOptionsPage(
1 -> { 1 -> {
soBundle = soBundle.copy( soBundle = soBundle.copy(
startAppUseCustom = true startAppUseCustom = true,
) )
startAppCustomInput = "" startAppCustomInput = ""
} }
@@ -1893,7 +1843,7 @@ internal fun ScrcpyAllOptionsPage(
) )
} else { } else {
soBundle.copy( soBundle.copy(
startAppCustom = startAppCustomInput startAppCustom = startAppCustomInput,
) )
} }
}, },
@@ -1935,8 +1885,8 @@ internal fun ScrcpyAllOptionsPage(
newDisplay = NewDisplay.parseFrom( newDisplay = NewDisplay.parseFrom(
newDisplayWidthInput, newDisplayWidthInput,
newDisplayHeightInput, newDisplayHeightInput,
newDisplayDpiInput newDisplayDpiInput,
).toString() ).toString(),
) )
}, },
singleLine = true, singleLine = true,
@@ -1958,8 +1908,8 @@ internal fun ScrcpyAllOptionsPage(
newDisplay = NewDisplay.parseFrom( newDisplay = NewDisplay.parseFrom(
newDisplayWidthInput, newDisplayWidthInput,
newDisplayHeightInput, newDisplayHeightInput,
newDisplayDpiInput newDisplayDpiInput,
).toString() ).toString(),
) )
}, },
singleLine = true, singleLine = true,
@@ -1981,8 +1931,8 @@ internal fun ScrcpyAllOptionsPage(
newDisplay = NewDisplay.parseFrom( newDisplay = NewDisplay.parseFrom(
newDisplayWidthInput, newDisplayWidthInput,
newDisplayHeightInput, newDisplayHeightInput,
newDisplayDpiInput newDisplayDpiInput,
).toString() ).toString(),
) )
}, },
singleLine = true, singleLine = true,
@@ -2023,7 +1973,7 @@ internal fun ScrcpyAllOptionsPage(
TextButton( TextButton(
text = stringResource( text = stringResource(
if (newDisplayWidthBlank || newDisplayHeightBlank) R.string.scrcpyopt_native if (newDisplayWidthBlank || newDisplayHeightBlank) R.string.scrcpyopt_native
else R.string.button_swap else R.string.button_swap,
), ),
onClick = { onClick = {
if (newDisplayWidthBlank || newDisplayHeightBlank) { if (newDisplayWidthBlank || newDisplayHeightBlank) {
@@ -2044,8 +1994,8 @@ internal fun ScrcpyAllOptionsPage(
newDisplay = NewDisplay.parseFrom( newDisplay = NewDisplay.parseFrom(
newDisplayWidthInput, newDisplayWidthInput,
newDisplayHeightInput, newDisplayHeightInput,
newDisplayDpiInput newDisplayDpiInput,
).toString() ).toString(),
) )
}, },
modifier = Modifier.width(trailingButtonWidth), modifier = Modifier.width(trailingButtonWidth),
@@ -2086,8 +2036,8 @@ internal fun ScrcpyAllOptionsPage(
cropWidthInput, cropWidthInput,
cropHeightInput, cropHeightInput,
cropXInput, cropXInput,
cropYInput cropYInput,
).toString() ).toString(),
) )
}, },
singleLine = true, singleLine = true,
@@ -2110,8 +2060,8 @@ internal fun ScrcpyAllOptionsPage(
cropWidthInput, cropWidthInput,
cropHeightInput, cropHeightInput,
cropXInput, cropXInput,
cropYInput cropYInput,
).toString() ).toString(),
) )
}, },
singleLine = true, singleLine = true,
@@ -2139,8 +2089,8 @@ internal fun ScrcpyAllOptionsPage(
cropWidthInput, cropWidthInput,
cropHeightInput, cropHeightInput,
cropXInput, cropXInput,
cropYInput cropYInput,
).toString() ).toString(),
) )
}, },
singleLine = true, singleLine = true,
@@ -2163,8 +2113,8 @@ internal fun ScrcpyAllOptionsPage(
cropWidthInput, cropWidthInput,
cropHeightInput, cropHeightInput,
cropXInput, cropXInput,
cropYInput cropYInput,
).toString() ).toString(),
) )
}, },
singleLine = true, singleLine = true,
@@ -2193,7 +2143,7 @@ internal fun ScrcpyAllOptionsPage(
selectedIndex = logLevelIndex, selectedIndex = logLevelIndex,
onSelectedIndexChange = { onSelectedIndexChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
logLevel = LogLevel.entries[it].string logLevel = LogLevel.entries[it].string,
) )
}, },
) )
@@ -2218,6 +2168,8 @@ private fun ProfileNameDialog(
onConfirm: (String, String?) -> Unit, onConfirm: (String, String?) -> Unit,
) { ) {
if (mode == null) return if (mode == null) return
val haptic = LocalHapticFeedback.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val textDefault = stringResource(R.string.text_default) val textDefault = stringResource(R.string.text_default)
@@ -2282,12 +2234,18 @@ private fun ProfileNameDialog(
) { ) {
TextButton( TextButton(
text = stringResource(R.string.button_cancel), text = stringResource(R.string.button_cancel),
onClick = onDismissRequest, onClick = {
haptic.contextClick()
onDismissRequest()
},
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
TextButton( TextButton(
text = stringResource(R.string.button_confirm), text = stringResource(R.string.button_confirm),
onClick = { onConfirm(input, copySourceProfileId) }, onClick = {
haptic.confirm()
onConfirm(input, copySourceProfileId)
},
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary(), colors = ButtonDefaults.textButtonColorsPrimary(),
) )
@@ -2345,14 +2303,14 @@ private fun ManageProfilesSheet(
icon = Icons.Rounded.Edit, icon = Icons.Rounded.Edit,
contentDescription = textRename, contentDescription = textRename,
onClick = { onRenameProfile(profile.id) }, onClick = { onRenameProfile(profile.id) },
) ),
) )
add( add(
ReorderableList.EndAction.Icon( ReorderableList.EndAction.Icon(
icon = Icons.Rounded.DeleteOutline, icon = Icons.Rounded.DeleteOutline,
contentDescription = textDelete, contentDescription = textDelete,
onClick = { onDeleteProfile(profile.id) }, onClick = { onDeleteProfile(profile.id) },
) ),
) )
} }
}, },
@@ -2372,9 +2330,10 @@ private fun DeleteProfileDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: () -> Unit, onConfirm: () -> Unit,
) { ) {
if (!show) return val haptic = LocalHapticFeedback.current
OverlayDialog( OverlayDialog(
show = true, show = show,
title = stringResource(R.string.scrcpyopt_delete_profile), title = stringResource(R.string.scrcpyopt_delete_profile),
summary = stringResource(R.string.scrcpyopt_delete_confirm, profileName), summary = stringResource(R.string.scrcpyopt_delete_confirm, profileName),
defaultWindowInsetsPadding = false, defaultWindowInsetsPadding = false,
@@ -2385,12 +2344,18 @@ private fun DeleteProfileDialog(
) { ) {
TextButton( TextButton(
text = stringResource(R.string.button_cancel), text = stringResource(R.string.button_cancel),
onClick = onDismissRequest, onClick = {
haptic.contextClick()
onDismissRequest()
},
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
TextButton( TextButton(
text = stringResource(R.string.button_delete), text = stringResource(R.string.button_delete),
onClick = onConfirm, onClick = {
haptic.confirm()
onConfirm()
},
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary(), colors = ButtonDefaults.textButtonColorsPrimary(),
) )

View File

@@ -9,32 +9,18 @@ import androidx.activity.compose.LocalActivity
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.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
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
import androidx.compose.material.icons.rounded.FileOpen import androidx.compose.material.icons.rounded.FileOpen
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
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.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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
@@ -45,9 +31,9 @@ import io.github.miuzarte.scrcpyforandroid.MainActivity
import io.github.miuzarte.scrcpyforandroid.R import io.github.miuzarte.scrcpyforandroid.R
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.nativecore.DirectAdbTransport import io.github.miuzarte.scrcpyforandroid.nativecore.DirectAdbTransport
import io.github.miuzarte.scrcpyforandroid.scaffolds.ArrowSlider
import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn
import io.github.miuzarte.scrcpyforandroid.scaffolds.SectionSmallTitle import io.github.miuzarte.scrcpyforandroid.scaffolds.SectionSmallTitle
import io.github.miuzarte.scrcpyforandroid.scaffolds.ArrowSlider
import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
@@ -57,26 +43,9 @@ import io.github.miuzarte.scrcpyforandroid.storage.AppSettings.FullscreenVirtual
import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage.adbClientData import io.github.miuzarte.scrcpyforandroid.storage.Storage.adbClientData
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.*
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur import kotlinx.coroutines.*
import io.github.miuzarte.scrcpyforandroid.ui.MonetKeyColorOptions import top.yukonga.miuix.kmp.basic.*
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.DropdownEntry
import top.yukonga.miuix.kmp.basic.DropdownItem
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.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
@@ -172,6 +141,7 @@ fun SettingsPage(
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val haptic = LocalHapticFeedback.current
val navigator = LocalRootNavigator.current val navigator = LocalRootNavigator.current
val serverPicker = LocalServerPicker.current val serverPicker = LocalServerPicker.current
val terminalFontPicker = LocalTerminalFontPicker.current val terminalFontPicker = LocalTerminalFontPicker.current
@@ -215,19 +185,19 @@ fun SettingsPage(
var serverRemotePathInput by rememberSaveable(asBundle.serverRemotePath) { var serverRemotePathInput by rememberSaveable(asBundle.serverRemotePath) {
mutableStateOf( mutableStateOf(
if (asBundle.serverRemotePath == AppSettings.SERVER_REMOTE_PATH.defaultValue) "" if (asBundle.serverRemotePath == AppSettings.SERVER_REMOTE_PATH.defaultValue) ""
else asBundle.serverRemotePath else asBundle.serverRemotePath,
) )
} }
var adbKeyNameInput by rememberSaveable(asBundle.adbKeyName) { var adbKeyNameInput by rememberSaveable(asBundle.adbKeyName) {
mutableStateOf( mutableStateOf(
if (asBundle.adbKeyName == AppSettings.ADB_KEY_NAME.defaultValue) "" if (asBundle.adbKeyName == AppSettings.ADB_KEY_NAME.defaultValue) ""
else asBundle.adbKeyName else asBundle.adbKeyName,
) )
} }
val adbPrivateKeyPicker = rememberLauncherForActivityResult( val adbPrivateKeyPicker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument() ActivityResultContracts.OpenDocument(),
) { uri: Uri? -> ) { uri: Uri? ->
if (uri == null) return@rememberLauncherForActivityResult if (uri == null) return@rememberLauncherForActivityResult
scope.launch { scope.launch {
@@ -247,7 +217,7 @@ fun SettingsPage(
} }
} }
val adbPublicKeyPicker = rememberLauncherForActivityResult( val adbPublicKeyPicker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument() ActivityResultContracts.OpenDocument(),
) { uri: Uri? -> ) { uri: Uri? ->
if (uri == null) return@rememberLauncherForActivityResult if (uri == null) return@rememberLauncherForActivityResult
scope.launch { scope.launch {
@@ -310,7 +280,7 @@ fun SettingsPage(
selected = lang.second == asBundle.languageTag, selected = lang.second == asBundle.languageTag,
onClick = { onClick = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
languageTag = lang.second languageTag = lang.second,
) )
MainActivity.setAppLanguageTag(context, lang.second) MainActivity.setAppLanguageTag(context, lang.second)
activity?.recreate() activity?.recreate()
@@ -328,7 +298,7 @@ fun SettingsPage(
.coerceIn(0, AppSettings.ThemeModes.baseOptions.lastIndex), .coerceIn(0, AppSettings.ThemeModes.baseOptions.lastIndex),
onSelectedIndexChange = { onSelectedIndexChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
themeBaseIndex = it themeBaseIndex = it,
) )
}, },
) )
@@ -338,7 +308,7 @@ fun SettingsPage(
checked = asBundle.monet, checked = asBundle.monet,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
monet = it monet = it,
) )
}, },
) )
@@ -352,7 +322,7 @@ fun SettingsPage(
.coerceIn(0, MonetKeyColorOptions.lastIndex), .coerceIn(0, MonetKeyColorOptions.lastIndex),
onSelectedIndexChange = { onSelectedIndexChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
monetSeedIndex = it monetSeedIndex = it,
) )
}, },
) )
@@ -368,7 +338,7 @@ fun SettingsPage(
.coerceIn(0, monetPaletteStyleOptions.lastIndex), .coerceIn(0, monetPaletteStyleOptions.lastIndex),
onSelectedIndexChange = { onSelectedIndexChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
monetPaletteStyle = it monetPaletteStyle = it,
) )
}, },
) )
@@ -380,7 +350,7 @@ fun SettingsPage(
.coerceIn(0, monetColorSpecOptions.lastIndex), .coerceIn(0, monetColorSpecOptions.lastIndex),
onSelectedIndexChange = { onSelectedIndexChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
monetColorSpec = it monetColorSpec = it,
) )
}, },
) )
@@ -392,9 +362,9 @@ fun SettingsPage(
checked = asBundle.blur, checked = asBundle.blur,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
blur = it blur = it,
) )
} },
) )
SwitchPreference( SwitchPreference(
title = stringResource(R.string.pref_title_floating_bottom_bar), title = stringResource(R.string.pref_title_floating_bottom_bar),
@@ -402,9 +372,9 @@ fun SettingsPage(
checked = asBundle.floatingBottomBar, checked = asBundle.floatingBottomBar,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
floatingBottomBar = it floatingBottomBar = it,
) )
} },
) )
AnimatedVisibility(asBundle.floatingBottomBar && asBundle.blur) { AnimatedVisibility(asBundle.floatingBottomBar && asBundle.blur) {
Column { Column {
@@ -415,9 +385,9 @@ fun SettingsPage(
&& asBundle.floatingBottomBarBlur, && asBundle.floatingBottomBarBlur,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
floatingBottomBarBlur = it floatingBottomBarBlur = it,
) )
} },
) )
} }
} }
@@ -435,7 +405,7 @@ fun SettingsPage(
onCheckedChange = { onCheckedChange = {
if (!isScrcpyStreaming) if (!isScrcpyStreaming)
asBundle = asBundle.copy( asBundle = asBundle.copy(
lowLatency = it lowLatency = it,
) )
}, },
) )
@@ -445,7 +415,7 @@ fun SettingsPage(
checked = asBundle.fullscreenDebugInfo, checked = asBundle.fullscreenDebugInfo,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenDebugInfo = it fullscreenDebugInfo = it,
) )
}, },
) )
@@ -455,7 +425,7 @@ fun SettingsPage(
checked = asBundle.hideSimpleConfigItems, checked = asBundle.hideSimpleConfigItems,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
hideSimpleConfigItems = it hideSimpleConfigItems = it,
) )
}, },
) )
@@ -466,7 +436,7 @@ fun SettingsPage(
onValueChange = { onValueChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
devicePreviewCardHeightDp = devicePreviewCardHeightDp =
it.roundToInt().coerceAtLeast(120) it.roundToInt().coerceAtLeast(120),
) )
}, },
valueRange = 160f..600f, valueRange = 160f..600f,
@@ -479,7 +449,7 @@ fun SettingsPage(
onInputConfirm = { input -> onInputConfirm = { input ->
input.toIntOrNull()?.let { input.toIntOrNull()?.let {
asBundle = asBundle.copy( asBundle = asBundle.copy(
devicePreviewCardHeightDp = it.coerceAtLeast(120) devicePreviewCardHeightDp = it.coerceAtLeast(120),
) )
} }
}, },
@@ -487,17 +457,24 @@ fun SettingsPage(
ArrowPreference( ArrowPreference(
title = stringResource(R.string.pref_title_quick_device_sort), title = stringResource(R.string.pref_title_quick_device_sort),
summary = stringResource(R.string.pref_summary_quick_device_sort), summary = stringResource(R.string.pref_summary_quick_device_sort),
onClick = onOpenReorderDevices, onClick = {
haptic.contextClick()
onOpenReorderDevices()
},
) )
ArrowPreference( ArrowPreference(
title = stringResource(R.string.pref_title_virtual_button_sort), title = stringResource(R.string.pref_title_virtual_button_sort),
summary = stringResource(R.string.pref_summary_virtual_button_sort), summary = stringResource(R.string.pref_summary_virtual_button_sort),
onClick = { navigator.push(RootScreen.VirtualButtonOrder) }, onClick = {
haptic.contextClick()
navigator.push(RootScreen.VirtualButtonOrder)
},
) )
ArrowPreference( ArrowPreference(
title = stringResource(R.string.pref_title_password_autofill), title = stringResource(R.string.pref_title_password_autofill),
summary = stringResource(R.string.pref_summary_password_autofill), summary = stringResource(R.string.pref_summary_password_autofill),
onClick = { onClick = {
haptic.contextClick()
context.startActivity(LockscreenPasswordActivity.createIntent(context)) context.startActivity(LockscreenPasswordActivity.createIntent(context))
}, },
) )
@@ -507,7 +484,7 @@ fun SettingsPage(
checked = asBundle.realtimeClipboardSyncToDevice, checked = asBundle.realtimeClipboardSyncToDevice,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
realtimeClipboardSyncToDevice = it realtimeClipboardSyncToDevice = it,
) )
}, },
) )
@@ -523,7 +500,7 @@ fun SettingsPage(
checked = asBundle.fullscreenControlIgnoreSystemRotationLock, checked = asBundle.fullscreenControlIgnoreSystemRotationLock,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenControlIgnoreSystemRotationLock = it fullscreenControlIgnoreSystemRotationLock = it,
) )
}, },
) )
@@ -533,7 +510,7 @@ fun SettingsPage(
checked = asBundle.fullscreenControlBackToDevice, checked = asBundle.fullscreenControlBackToDevice,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenControlBackToDevice = it fullscreenControlBackToDevice = it,
) )
}, },
) )
@@ -543,7 +520,7 @@ fun SettingsPage(
checked = asBundle.showFullscreenVirtualButtons, checked = asBundle.showFullscreenVirtualButtons,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
showFullscreenVirtualButtons = it showFullscreenVirtualButtons = it,
) )
}, },
) )
@@ -564,7 +541,7 @@ fun SettingsPage(
modeIndex = index, modeIndex = index,
directionIndex = fullscreenVirtualButtonDock.directionIndex, directionIndex = fullscreenVirtualButtonDock.directionIndex,
) )
.toStoredValue() .toStoredValue(),
) )
}, },
) )
@@ -583,7 +560,7 @@ fun SettingsPage(
modeIndex = fullscreenVirtualButtonDock.modeIndex, modeIndex = fullscreenVirtualButtonDock.modeIndex,
directionIndex = index, directionIndex = index,
) )
.toStoredValue() .toStoredValue(),
) )
}, },
) )
@@ -593,7 +570,7 @@ fun SettingsPage(
title = stringResource(R.string.pref_title_virtual_button_direction), title = stringResource(R.string.pref_title_virtual_button_direction),
summary = stringResource( summary = stringResource(
if (fullscreenVirtualButtonDock.isFixed) R.string.dock_fixed if (fullscreenVirtualButtonDock.isFixed) R.string.dock_fixed
else R.string.dock_follow else R.string.dock_follow,
) + ) +
stringResource(R.string.dock_display_on) + stringResource(R.string.dock_display_on) +
stringResource(fullscreenVirtualButtonDock.directionLabelResId), stringResource(fullscreenVirtualButtonDock.directionLabelResId),
@@ -604,7 +581,7 @@ fun SettingsPage(
onValueChange = { onValueChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenVirtualButtonHeightDp = fullscreenVirtualButtonHeightDp =
it.roundToInt().coerceIn(16, 80) it.roundToInt().coerceIn(16, 80),
) )
}, },
valueRange = 16f..80f, valueRange = 16f..80f,
@@ -618,7 +595,7 @@ fun SettingsPage(
input.toIntOrNull()?.let { input.toIntOrNull()?.let {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenVirtualButtonHeightDp = fullscreenVirtualButtonHeightDp =
it.coerceIn(1, 160) it.coerceIn(1, 160),
) )
} }
}, },
@@ -631,7 +608,7 @@ fun SettingsPage(
checked = asBundle.showFullscreenFloatingButton, checked = asBundle.showFullscreenFloatingButton,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
showFullscreenFloatingButton = it showFullscreenFloatingButton = it,
) )
}, },
) )
@@ -643,7 +620,7 @@ fun SettingsPage(
onValueChange = { onValueChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenFloatingButtonSizeDp = fullscreenFloatingButtonSizeDp =
it.roundToInt().coerceIn(32, 64) it.roundToInt().coerceIn(32, 64),
) )
}, },
valueRange = 32f..64f, valueRange = 32f..64f,
@@ -657,7 +634,7 @@ fun SettingsPage(
input.toIntOrNull()?.let { input.toIntOrNull()?.let {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenFloatingButtonSizeDp = fullscreenFloatingButtonSizeDp =
it.coerceIn(16, 96) it.coerceIn(16, 96),
) )
} }
}, },
@@ -668,7 +645,7 @@ fun SettingsPage(
onValueChange = { onValueChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenFloatingButtonBackgroundAlphaPercent = fullscreenFloatingButtonBackgroundAlphaPercent =
it.roundToInt().coerceIn(10, 100) it.roundToInt().coerceIn(10, 100),
) )
}, },
valueRange = 10f..100f, valueRange = 10f..100f,
@@ -682,7 +659,7 @@ fun SettingsPage(
input.toIntOrNull()?.let { input.toIntOrNull()?.let {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenFloatingButtonBackgroundAlphaPercent = fullscreenFloatingButtonBackgroundAlphaPercent =
it.coerceIn(10, 100) it.coerceIn(10, 100),
) )
} }
}, },
@@ -693,7 +670,7 @@ fun SettingsPage(
onValueChange = { onValueChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenFloatingButtonRingAlphaPercent = fullscreenFloatingButtonRingAlphaPercent =
it.roundToInt().coerceIn(0, 100) it.roundToInt().coerceIn(0, 100),
) )
}, },
valueRange = 0f..100f, valueRange = 0f..100f,
@@ -707,7 +684,7 @@ fun SettingsPage(
input.toIntOrNull()?.let { input.toIntOrNull()?.let {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenFloatingButtonRingAlphaPercent = fullscreenFloatingButtonRingAlphaPercent =
it.coerceIn(0, 100) it.coerceIn(0, 100),
) )
} }
}, },
@@ -720,7 +697,7 @@ fun SettingsPage(
checked = asBundle.fullscreenCompatibilityMode, checked = asBundle.fullscreenCompatibilityMode,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
fullscreenCompatibilityMode = it fullscreenCompatibilityMode = it,
) )
}, },
) )
@@ -752,11 +729,12 @@ fun SettingsPage(
trailingIcon = { trailingIcon = {
Row( Row(
modifier = Modifier modifier = Modifier
.padding(end = UiSpacing.Medium) .padding(end = UiSpacing.Medium),
) { ) {
if (asBundle.customServerUri.isNotBlank()) if (asBundle.customServerUri.isNotBlank())
IconButton( IconButton(
onClick = { onClick = {
haptic.contextClick()
asBundle = asBundle.copy( asBundle = asBundle.copy(
customServerUri = "", customServerUri = "",
customServerVersion = "", customServerVersion = "",
@@ -768,7 +746,12 @@ fun SettingsPage(
contentDescription = stringResource(R.string.cd_clear), contentDescription = stringResource(R.string.cd_clear),
) )
} }
IconButton(onClick = serverPicker.pick) { IconButton(
onClick = {
haptic.contextClick()
serverPicker.pick()
},
) {
Icon( Icon(
imageVector = Icons.Rounded.FileOpen, imageVector = Icons.Rounded.FileOpen,
contentDescription = stringResource(R.string.cd_select_file), contentDescription = stringResource(R.string.cd_select_file),
@@ -795,7 +778,7 @@ fun SettingsPage(
customServerVersionInput = "" customServerVersionInput = ""
asBundle = asBundle.copy( asBundle = asBundle.copy(
customServerVersion = customServerVersionInput customServerVersion = customServerVersionInput
.ifBlank { AppSettings.CUSTOM_SERVER_VERSION.defaultValue } .ifBlank { AppSettings.CUSTOM_SERVER_VERSION.defaultValue },
) )
}, },
label = Scrcpy.DEFAULT_SERVER_VERSION, label = Scrcpy.DEFAULT_SERVER_VERSION,
@@ -821,7 +804,7 @@ fun SettingsPage(
serverRemotePathInput = "" serverRemotePathInput = ""
asBundle = asBundle.copy( asBundle = asBundle.copy(
serverRemotePath = serverRemotePathInput serverRemotePath = serverRemotePathInput
.ifBlank { AppSettings.SERVER_REMOTE_PATH.defaultValue } .ifBlank { AppSettings.SERVER_REMOTE_PATH.defaultValue },
) )
}, },
label = Scrcpy.DEFAULT_REMOTE_PATH, label = Scrcpy.DEFAULT_REMOTE_PATH,
@@ -842,6 +825,7 @@ fun SettingsPage(
title = stringResource(R.string.pref_title_battery_optimization), title = stringResource(R.string.pref_title_battery_optimization),
summary = stringResource(R.string.pref_summary_battery_optimization), summary = stringResource(R.string.pref_summary_battery_optimization),
onClick = { onClick = {
haptic.contextClick()
val appInfoArgs = Bundle().apply { val appInfoArgs = Bundle().apply {
putString("package", context.packageName) putString("package", context.packageName)
putInt("uid", context.applicationInfo.uid) putInt("uid", context.applicationInfo.uid)
@@ -866,10 +850,10 @@ fun SettingsPage(
} }
val requestIntent = Intent( val requestIntent = Intent(
AndroidSettings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, AndroidSettings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
"package:${context.packageName}".toUri() "package:${context.packageName}".toUri(),
) )
val fallbackIntent = Intent( val fallbackIntent = Intent(
AndroidSettings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS AndroidSettings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS,
) )
runCatching { context.startActivity(appDetailsIntent) } runCatching { context.startActivity(appDetailsIntent) }
.recoverCatching { context.startActivity(requestIntent) } .recoverCatching { context.startActivity(requestIntent) }
@@ -897,7 +881,7 @@ fun SettingsPage(
adbKeyNameInput = "" adbKeyNameInput = ""
asBundle = asBundle.copy( asBundle = asBundle.copy(
adbKeyName = adbKeyNameInput adbKeyName = adbKeyNameInput
.ifBlank { AppSettings.ADB_KEY_NAME.defaultValue } .ifBlank { AppSettings.ADB_KEY_NAME.defaultValue },
) )
}, },
label = AppSettings.ADB_KEY_NAME.defaultValue, label = AppSettings.ADB_KEY_NAME.defaultValue,
@@ -931,7 +915,7 @@ fun SettingsPage(
R.string.pref_adb_key_imported R.string.pref_adb_key_imported
else -> R.string.pref_adb_key_generated else -> R.string.pref_adb_key_generated
} },
), ),
useLabelAsPlaceholder = true, useLabelAsPlaceholder = true,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -939,6 +923,7 @@ fun SettingsPage(
Row(modifier = Modifier.padding(end = UiSpacing.Medium)) { Row(modifier = Modifier.padding(end = UiSpacing.Medium)) {
IconButton( IconButton(
onClick = { onClick = {
haptic.contextClick()
scope.launch { scope.launch {
runCatching { runCatching {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -972,7 +957,10 @@ fun SettingsPage(
) )
} }
IconButton( IconButton(
onClick = { adbPrivateKeyPicker.launch(arrayOf("*/*")) }, onClick = {
haptic.contextClick()
adbPrivateKeyPicker.launch(arrayOf("*/*"))
},
) { ) {
Icon( Icon(
imageVector = Icons.Rounded.FileOpen, imageVector = Icons.Rounded.FileOpen,
@@ -997,7 +985,7 @@ fun SettingsPage(
R.string.pref_adb_key_imported R.string.pref_adb_key_imported
else -> R.string.pref_adb_key_generated else -> R.string.pref_adb_key_generated
} },
) )
Text( Text(
text = stringResource(R.string.pref_title_adb_public_key), text = stringResource(R.string.pref_title_adb_public_key),
@@ -1013,7 +1001,10 @@ fun SettingsPage(
trailingIcon = { trailingIcon = {
Row(modifier = Modifier.padding(end = UiSpacing.Medium)) { Row(modifier = Modifier.padding(end = UiSpacing.Medium)) {
IconButton( IconButton(
onClick = { adbPublicKeyPicker.launch(arrayOf("*/*")) }, onClick = {
haptic.contextClick()
adbPublicKeyPicker.launch(arrayOf("*/*"))
},
) { ) {
Icon( Icon(
imageVector = Icons.Rounded.FileOpen, imageVector = Icons.Rounded.FileOpen,
@@ -1031,7 +1022,7 @@ fun SettingsPage(
checked = asBundle.adbPairingAutoDiscoverOnDialogOpen, checked = asBundle.adbPairingAutoDiscoverOnDialogOpen,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
adbPairingAutoDiscoverOnDialogOpen = it adbPairingAutoDiscoverOnDialogOpen = it,
) )
}, },
) )
@@ -1041,7 +1032,7 @@ fun SettingsPage(
checked = asBundle.adbAutoReconnectPairedDevice, checked = asBundle.adbAutoReconnectPairedDevice,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
adbAutoReconnectPairedDevice = it adbAutoReconnectPairedDevice = it,
) )
}, },
) )
@@ -1051,10 +1042,10 @@ fun SettingsPage(
checked = asBundle.adbAutoLoadAppListOnConnect, checked = asBundle.adbAutoLoadAppListOnConnect,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
adbAutoLoadAppListOnConnect = it adbAutoLoadAppListOnConnect = it,
) )
if (it) AppRuntime.snackbar( if (it) AppRuntime.snackbar(
R.string.pref_warning_list_apps R.string.pref_warning_list_apps,
) )
}, },
) )
@@ -1070,7 +1061,7 @@ fun SettingsPage(
value = asBundle.terminalFontSizeSp, value = asBundle.terminalFontSizeSp,
onValueChange = { onValueChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
terminalFontSizeSp = it.roundToInt().toFloat() terminalFontSizeSp = it.roundToInt().toFloat(),
) )
}, },
valueRange = 1f..32f, valueRange = 1f..32f,
@@ -1083,7 +1074,7 @@ fun SettingsPage(
onInputConfirm = { input -> onInputConfirm = { input ->
input.toIntOrNull()?.let { input.toIntOrNull()?.let {
asBundle = asBundle.copy( asBundle = asBundle.copy(
terminalFontSizeSp = it.coerceIn(1, 32).toFloat() terminalFontSizeSp = it.coerceIn(1, 32).toFloat(),
) )
} }
}, },
@@ -1112,14 +1103,15 @@ fun SettingsPage(
if (asBundle.terminalFontDisplayName.isNotBlank()) { if (asBundle.terminalFontDisplayName.isNotBlank()) {
IconButton( IconButton(
onClick = { onClick = {
haptic.contextClick()
scope.launch { scope.launch {
val cleared = clearTerminalFont(context) val cleared = clearTerminalFont(context)
asBundle = asBundle.copy( asBundle = asBundle.copy(
terminalFontDisplayName = "" terminalFontDisplayName = "",
) )
AppRuntime.snackbar( AppRuntime.snackbar(
if (cleared) R.string.pref_font_restored if (cleared) R.string.pref_font_restored
else R.string.pref_no_custom_font else R.string.pref_no_custom_font,
) )
} }
}, },
@@ -1130,7 +1122,12 @@ fun SettingsPage(
) )
} }
} }
IconButton(onClick = terminalFontPicker.pick) { IconButton(
onClick = {
haptic.contextClick()
terminalFontPicker.pick()
},
) {
Icon( Icon(
imageVector = Icons.Rounded.FileOpen, imageVector = Icons.Rounded.FileOpen,
contentDescription = stringResource(R.string.cd_select_font), contentDescription = stringResource(R.string.cd_select_font),
@@ -1153,7 +1150,7 @@ fun SettingsPage(
checked = asBundle.clearLogsOnExit, checked = asBundle.clearLogsOnExit,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
clearLogsOnExit = it clearLogsOnExit = it,
) )
}, },
) )
@@ -1163,7 +1160,7 @@ fun SettingsPage(
checked = asBundle.hideDeviceLogs, checked = asBundle.hideDeviceLogs,
onCheckedChange = { onCheckedChange = {
asBundle = asBundle.copy( asBundle = asBundle.copy(
hideDeviceLogs = it hideDeviceLogs = it,
) )
}, },
) )
@@ -1176,7 +1173,10 @@ fun SettingsPage(
ArrowPreference( ArrowPreference(
title = stringResource(R.string.about_title), title = stringResource(R.string.about_title),
summary = updateSummary, summary = updateSummary,
onClick = { navigator.push(RootScreen.About) }, onClick = {
haptic.contextClick()
navigator.push(RootScreen.About)
},
) )
} }
} }

View File

@@ -19,6 +19,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.github.miuzarte.scrcpyforandroid.R import io.github.miuzarte.scrcpyforandroid.R
import io.github.miuzarte.scrcpyforandroid.ui.confirm
import io.github.miuzarte.scrcpyforandroid.ui.contextClick import io.github.miuzarte.scrcpyforandroid.ui.contextClick
import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.ButtonDefaults
import top.yukonga.miuix.kmp.basic.Slider import top.yukonga.miuix.kmp.basic.Slider
@@ -174,7 +175,7 @@ private fun SliderInputDialog(
TextButton( TextButton(
text = stringResource(R.string.button_confirm), text = stringResource(R.string.button_confirm),
onClick = { onClick = {
haptic.contextClick() haptic.confirm()
val inputValue = text.toFloatOrNull() ?: 0f val inputValue = text.toFloatOrNull() ?: 0f
if (inputValue >= inputValueRange.start && inputValue <= inputValueRange.endInclusive) { if (inputValue >= inputValueRange.start && inputValue <= inputValueRange.endInclusive) {
onConfirm(text.trim()) onConfirm(text.trim())

View File

@@ -10,44 +10,16 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AddLink import androidx.compose.material.icons.rounded.*
import androidx.compose.material.icons.rounded.CheckCircleOutline import androidx.compose.runtime.*
import androidx.compose.material.icons.rounded.Fullscreen
import androidx.compose.material.icons.rounded.LinkOff
import androidx.compose.material.icons.rounded.Wifi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@@ -89,6 +61,7 @@ import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions
import io.github.miuzarte.scrcpyforandroid.storage.Settings import io.github.miuzarte.scrcpyforandroid.storage.Settings
import io.github.miuzarte.scrcpyforandroid.storage.Storage import io.github.miuzarte.scrcpyforandroid.storage.Storage
import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions
import io.github.miuzarte.scrcpyforandroid.ui.confirm
import io.github.miuzarte.scrcpyforandroid.ui.contextClick import io.github.miuzarte.scrcpyforandroid.ui.contextClick
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -98,21 +71,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.*
import top.yukonga.miuix.kmp.basic.ButtonDefaults
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.overlay.OverlayDialog import top.yukonga.miuix.kmp.overlay.OverlayDialog
import top.yukonga.miuix.kmp.preference.ArrowPreference import top.yukonga.miuix.kmp.preference.*
import top.yukonga.miuix.kmp.preference.CheckboxLocation
import top.yukonga.miuix.kmp.preference.CheckboxPreference
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
import top.yukonga.miuix.kmp.preference.SwitchPreference
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme 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.theme.MiuixTheme.textStyles import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles
@@ -230,6 +191,8 @@ internal fun PairingCard(
onDiscoverTarget: (suspend () -> Pair<String, Int>?)? = null, onDiscoverTarget: (suspend () -> Pair<String, Int>?)? = null,
onPair: (host: String, port: String, code: String) -> Unit, onPair: (host: String, port: String, code: String) -> Unit,
) { ) {
val haptic = LocalHapticFeedback.current
val showPairDialog = remember { mutableStateOf(false) } val showPairDialog = remember { mutableStateOf(false) }
val holdDownState = remember { mutableStateOf(false) } val holdDownState = remember { mutableStateOf(false) }
@@ -237,6 +200,7 @@ internal fun PairingCard(
ArrowPreference( ArrowPreference(
title = stringResource(R.string.device_pairing_title), title = stringResource(R.string.device_pairing_title),
onClick = { onClick = {
haptic.contextClick()
showPairDialog.value = true showPairDialog.value = true
holdDownState.value = true holdDownState.value = true
}, },
@@ -295,7 +259,7 @@ internal fun PreviewCard(
var nextPointerLabel by rememberSaveable { mutableIntStateOf(1) } var nextPointerLabel by rememberSaveable { mutableIntStateOf(1) }
val alpha by animateFloatAsState( val alpha by animateFloatAsState(
if (previewControlsVisible) 1f else 0f, if (previewControlsVisible) 1f else 0f,
label = "preview-controls" label = "preview-controls",
) )
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
val bringIntoViewRequester = remember { BringIntoViewRequester() } val bringIntoViewRequester = remember { BringIntoViewRequester() }
@@ -361,7 +325,7 @@ internal fun PreviewCard(
Card( Card(
modifier = Modifier modifier = Modifier
.bringIntoViewRequester(bringIntoViewRequester) .bringIntoViewRequester(bringIntoViewRequester)
.then(modifier) .then(modifier),
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -373,17 +337,20 @@ internal fun PreviewCard(
when (event.actionMasked) { when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> onTouchActiveChanged(true) MotionEvent.ACTION_DOWN -> onTouchActiveChanged(true)
MotionEvent.ACTION_UP, MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> onTouchActiveChanged(false) MotionEvent.ACTION_CANCEL,
-> onTouchActiveChanged(false)
} }
touchEventHandler.handleMotionEvent(event) touchEventHandler.handleMotionEvent(event)
} }
} else { } else {
Modifier.pointerInput(sessionInfo) { Modifier.pointerInput(sessionInfo) {
detectTapGestures(onTap = { detectTapGestures(
previewControlsVisible = !previewControlsVisible onTap = {
}) previewControlsVisible = !previewControlsVisible
},
)
} }
} },
) )
.onSizeChanged { touchAreaSize = it }, .onSizeChanged { touchAreaSize = it },
) { ) {
@@ -496,7 +463,6 @@ internal fun ConfigPanel(
recentTasksEndActionText: String, recentTasksEndActionText: String,
onOpenRecentTasks: () -> Unit, onOpenRecentTasks: () -> Unit,
onOpenAdvanced: () -> Unit, onOpenAdvanced: () -> Unit,
onStartStopHaptic: () -> Unit,
onStart: () -> Unit, onStart: () -> Unit,
onStop: () -> Unit, onStop: () -> Unit,
sessionInfo: Scrcpy.Session.SessionInfo?, sessionInfo: Scrcpy.Session.SessionInfo?,
@@ -505,6 +471,7 @@ internal fun ConfigPanel(
onOpenFullscreen: () -> Unit = {}, onOpenFullscreen: () -> Unit = {},
reverseSideActions: Boolean = false, reverseSideActions: Boolean = false,
) { ) {
val haptic = LocalHapticFeedback.current
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
val sessionStarted = sessionInfo != null val sessionStarted = sessionInfo != null
@@ -563,7 +530,7 @@ internal fun ConfigPanel(
checked = soBundle.audio, checked = soBundle.audio,
onCheckedChange = { onCheckedChange = {
soBundle = soBundle.copy( soBundle = soBundle.copy(
audio = it audio = it,
) )
}, },
enabled = !sessionStarted enabled = !sessionStarted
@@ -578,7 +545,7 @@ internal fun ConfigPanel(
onSelectedIndexChange = { onSelectedIndexChange = {
val codec = Codec.AUDIO[it] val codec = Codec.AUDIO[it]
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioCodec = codec.string audioCodec = codec.string,
) )
if (codec == Codec.FLAC) if (codec == Codec.FLAC)
AppRuntime.snackbar(R.string.device_config_audio_codec_note) AppRuntime.snackbar(R.string.device_config_audio_codec_note)
@@ -596,7 +563,7 @@ internal fun ConfigPanel(
val idx = value.roundToInt() val idx = value.roundToInt()
.coerceIn(0, ScrcpyPresets.AudioBitRate.lastIndex) .coerceIn(0, ScrcpyPresets.AudioBitRate.lastIndex)
soBundle = soBundle.copy( soBundle = soBundle.copy(
audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000 audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000,
) )
}, },
valueRange = 0f..ScrcpyPresets.AudioBitRate.lastIndex.toFloat(), valueRange = 0f..ScrcpyPresets.AudioBitRate.lastIndex.toFloat(),
@@ -626,7 +593,7 @@ internal fun ConfigPanel(
onSelectedIndexChange = { onSelectedIndexChange = {
val codec = Codec.VIDEO[it] val codec = Codec.VIDEO[it]
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoCodec = codec.string videoCodec = codec.string,
) )
if (codec == Codec.AV1) if (codec == Codec.AV1)
AppRuntime.snackbar(R.string.device_config_video_codec_note) AppRuntime.snackbar(R.string.device_config_video_codec_note)
@@ -639,7 +606,7 @@ internal fun ConfigPanel(
value = soBundle.videoBitRate / 1_000_000f, value = soBundle.videoBitRate / 1_000_000f,
onValueChange = { mbps -> onValueChange = { mbps ->
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoBitRate = (mbps * 10).roundToInt() * (1_000_000 / 10) videoBitRate = (mbps * 10).roundToInt() * (1_000_000 / 10),
) )
}, },
valueRange = 0f..40f, valueRange = 0f..40f,
@@ -669,7 +636,7 @@ internal fun ConfigPanel(
raw.toFloatOrNull()?.let { parsed -> raw.toFloatOrNull()?.let { parsed ->
if (parsed >= 0f) { if (parsed >= 0f) {
soBundle = soBundle.copy( soBundle = soBundle.copy(
videoBitRate = (parsed * 1_000_000f).roundToInt() videoBitRate = (parsed * 1_000_000f).roundToInt(),
) )
} }
} }
@@ -681,7 +648,7 @@ internal fun ConfigPanel(
ArrowPreference( ArrowPreference(
title = stringResource( title = stringResource(
if (!hideSimpleConfigItems) R.string.device_config_more_params if (!hideSimpleConfigItems) R.string.device_config_more_params
else R.string.device_config_all_params else R.string.device_config_all_params,
), ),
summary = stringResource(R.string.device_config_all_scrcpy_params), summary = stringResource(R.string.device_config_all_scrcpy_params),
endActions = { endActions = {
@@ -691,7 +658,10 @@ internal fun ConfigPanel(
fontSize = textStyles.body2.fontSize, fontSize = textStyles.body2.fontSize,
) )
}, },
onClick = onOpenAdvanced, onClick = {
haptic.contextClick()
onOpenAdvanced()
},
enabled = !sessionStarted, enabled = !sessionStarted,
) )
@@ -704,7 +674,10 @@ internal fun ConfigPanel(
fontSize = textStyles.body2.fontSize, fontSize = textStyles.body2.fontSize,
) )
}, },
onClick = onOpenAllApps, onClick = {
haptic.contextClick()
onOpenAllApps()
},
enabled = !busy && !adbConnecting, enabled = !busy && !adbConnecting,
) )
ArrowPreference( ArrowPreference(
@@ -716,7 +689,10 @@ internal fun ConfigPanel(
fontSize = textStyles.body2.fontSize, fontSize = textStyles.body2.fontSize,
) )
}, },
onClick = onOpenRecentTasks, onClick = {
haptic.contextClick()
onOpenRecentTasks()
},
enabled = !busy && !adbConnecting, enabled = !busy && !adbConnecting,
) )
@@ -738,7 +714,7 @@ internal fun ConfigPanel(
if (isQuickConnected) TextButton( if (isQuickConnected) TextButton(
text = stringResource(R.string.button_disconnect), text = stringResource(R.string.button_disconnect),
onClick = { onClick = {
onStartStopHaptic() haptic.contextClick()
onDisconnect() onDisconnect()
}, },
modifier = Modifier.weight(sideButtonWeight), modifier = Modifier.weight(sideButtonWeight),
@@ -751,7 +727,7 @@ internal fun ConfigPanel(
if (!sessionStarted) TextButton( if (!sessionStarted) TextButton(
text = stringResource(R.string.button_start), text = stringResource(R.string.button_start),
onClick = { onClick = {
onStartStopHaptic() haptic.contextClick()
onStart() onStart()
}, },
modifier = Modifier.weight(mainButtonWeight), modifier = Modifier.weight(mainButtonWeight),
@@ -761,7 +737,7 @@ internal fun ConfigPanel(
if (sessionStarted) TextButton( if (sessionStarted) TextButton(
text = stringResource(R.string.button_stop), text = stringResource(R.string.button_stop),
onClick = { onClick = {
onStartStopHaptic() haptic.contextClick()
onStop() onStop()
}, },
modifier = Modifier.weight(mainButtonWeight), modifier = Modifier.weight(mainButtonWeight),
@@ -774,7 +750,7 @@ internal fun ConfigPanel(
if (showFullscreenAction) TextButton( if (showFullscreenAction) TextButton(
text = stringResource(R.string.button_fullscreen), text = stringResource(R.string.button_fullscreen),
onClick = { onClick = {
onStartStopHaptic() haptic.contextClick()
onOpenFullscreen() onOpenFullscreen()
}, },
modifier = Modifier.weight(sideButtonWeight), modifier = Modifier.weight(sideButtonWeight),
@@ -818,6 +794,8 @@ private fun PairingDialog(
onDismissFinished: () -> Unit, onDismissFinished: () -> Unit,
onConfirm: (host: String, port: String, code: String) -> Unit, onConfirm: (host: String, port: String, code: String) -> Unit,
) { ) {
val haptic = LocalHapticFeedback.current
var host by rememberSaveable(showDialog) { mutableStateOf("") } var host by rememberSaveable(showDialog) { mutableStateOf("") }
var port by rememberSaveable(showDialog) { mutableStateOf("") } var port by rememberSaveable(showDialog) { mutableStateOf("") }
var code by rememberSaveable(showDialog) { mutableStateOf("") } var code by rememberSaveable(showDialog) { mutableStateOf("") }
@@ -869,7 +847,7 @@ private fun PairingDialog(
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onNext = { focusManager.moveFocus(FocusDirection.Next) }, onNext = { focusManager.moveFocus(FocusDirection.Next) },
), ),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
TextField( TextField(
value = port, value = port,
@@ -883,7 +861,7 @@ private fun PairingDialog(
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onNext = { focusManager.moveFocus(FocusDirection.Next) }, onNext = { focusManager.moveFocus(FocusDirection.Next) },
), ),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
TextField( TextField(
value = code, value = code,
@@ -897,7 +875,7 @@ private fun PairingDialog(
keyboardActions = KeyboardActions( keyboardActions = KeyboardActions(
onDone = { focusManager.clearFocus() }, onDone = { focusManager.clearFocus() },
), ),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
} }
@@ -909,9 +887,10 @@ private fun PairingDialog(
TextButton( TextButton(
text = stringResource( text = stringResource(
if (!discoveringPort) R.string.button_auto_discover if (!discoveringPort) R.string.button_auto_discover
else R.string.button_discovering else R.string.button_discovering,
), ),
onClick = { onClick = {
haptic.contextClick()
if (enabled && onDiscoverTarget != null && !discoveringPort) if (enabled && onDiscoverTarget != null && !discoveringPort)
scope.launch { doDiscover() } scope.launch { doDiscover() }
}, },
@@ -924,6 +903,7 @@ private fun PairingDialog(
TextButton( TextButton(
text = stringResource(R.string.button_cancel), text = stringResource(R.string.button_cancel),
onClick = { onClick = {
haptic.contextClick()
onDismissRequest() onDismissRequest()
}, },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@@ -931,6 +911,7 @@ private fun PairingDialog(
TextButton( TextButton(
text = stringResource(R.string.button_pair), text = stringResource(R.string.button_pair),
onClick = { onClick = {
haptic.confirm()
onConfirm(host.trim(), port.trim(), code.trim()) onConfirm(host.trim(), port.trim(), code.trim())
onDismissRequest() onDismissRequest()
}, },
@@ -1053,7 +1034,7 @@ fun ScrcpyVideoSurface(
factory = { context -> factory = { context ->
ScrcpyInputSurfaceView(context).apply { ScrcpyInputSurfaceView(context).apply {
currentSurfaceView = this currentSurfaceView = this
inputCallbacks = object : ScrcpyInputSurfaceView.InputCallbacks { inputCallbacks = object: ScrcpyInputSurfaceView.InputCallbacks {
override fun handleKeyEvent(event: KeyEvent): Boolean { override fun handleKeyEvent(event: KeyEvent): Boolean {
val handler = onImeKeyEvent ?: return false val handler = onImeKeyEvent ?: return false
taskScope.launch { taskScope.launch {
@@ -1076,7 +1057,7 @@ fun ScrcpyVideoSurface(
override fun handleDeleteSurroundingText( override fun handleDeleteSurroundingText(
beforeLength: Int, beforeLength: Int,
afterLength: Int afterLength: Int,
): Boolean { ): Boolean {
val handler = onImeDeleteSurroundingText ?: return false val handler = onImeDeleteSurroundingText ?: return false
taskScope.launch { taskScope.launch {
@@ -1087,46 +1068,48 @@ fun ScrcpyVideoSurface(
return true return true
} }
} }
holder.addCallback(object : SurfaceHolder.Callback { holder.addCallback(
override fun surfaceCreated(holder: SurfaceHolder) { object: SurfaceHolder.Callback {
val newSurface = holder.surface override fun surfaceCreated(holder: SurfaceHolder) {
if (!newSurface.isValid) return val newSurface = holder.surface
currentSurface = newSurface if (!newSurface.isValid) return
// Register immediately when surface becomes available currentSurface = newSurface
if (latestSession != null) { // Register immediately when surface becomes available
scope.launch { if (latestSession != null) {
NativeCoreFacade.attachVideoSurface(newSurface) scope.launch {
NativeCoreFacade.attachVideoSurface(newSurface)
}
} }
} }
}
override fun surfaceChanged( override fun surfaceChanged(
holder: SurfaceHolder, holder: SurfaceHolder,
format: Int, format: Int,
width: Int, width: Int,
height: Int, height: Int,
) { ) {
if (width <= 0 || height <= 0) return if (width <= 0 || height <= 0) return
if (!holder.surface.isValid) return if (!holder.surface.isValid) return
val surface = holder.surface val surface = holder.surface
currentSurface = surface currentSurface = surface
if (latestSession != null) { if (latestSession != null) {
scope.launch { scope.launch {
NativeCoreFacade.attachVideoSurface(surface) NativeCoreFacade.attachVideoSurface(surface)
}
} }
} }
}
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
val surface = currentSurface val surface = currentSurface
if (surface != null) { if (surface != null) {
taskScope.launch { taskScope.launch {
NativeCoreFacade.detachVideoSurface(surface) NativeCoreFacade.detachVideoSurface(surface)
}
currentSurface = null
} }
currentSurface = null
} }
} },
}) )
} }
}, },
update = {}, update = {},
@@ -1201,7 +1184,6 @@ internal fun DeviceTile(
else 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 = haptic::contextClick,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -1209,10 +1191,13 @@ internal fun DeviceTile(
.then( .then(
if (!isConnected) if (!isConnected)
Modifier.combinedClickable( Modifier.combinedClickable(
onClick = onClick, onClick = {
haptic.contextClick()
onClick()
},
onLongClick = onLongClick, onLongClick = onLongClick,
) )
else Modifier else Modifier,
) )
.padding(UiSpacing.PageItem), .padding(UiSpacing.PageItem),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -1261,7 +1246,7 @@ internal fun DeviceTile(
TextButton( TextButton(
text = stringResource( text = stringResource(
if (!isConnected) R.string.button_connect if (!isConnected) R.string.button_connect
else R.string.button_disconnect else R.string.button_disconnect,
), ),
onClick = onAction, onClick = onAction,
enabled = actionEnabled && !actionInProgress, enabled = actionEnabled && !actionInProgress,
@@ -1303,7 +1288,7 @@ internal fun DeviceTile(
onValueChange = { onValueChange = {
draftPortText = it.filter(Char::isDigit) draftPortText = it.filter(Char::isDigit)
draft = currentDraft.copy( draft = currentDraft.copy(
port = draftPortText?.toIntOrNull() ?: Defaults.ADB_PORT port = draftPortText?.toIntOrNull() ?: Defaults.ADB_PORT,
) )
}, },
label = stringResource(R.string.label_port), label = stringResource(R.string.label_port),
@@ -1473,13 +1458,19 @@ internal fun QuickConnectCard(
) { ) {
TextButton( TextButton(
text = stringResource(R.string.button_add_device), text = stringResource(R.string.button_add_device),
onClick = onAddDevice, onClick = {
haptic.contextClick()
onAddDevice()
},
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
enabled = enabled, enabled = enabled,
) )
TextButton( TextButton(
text = stringResource(R.string.button_direct_connect), text = stringResource(R.string.button_direct_connect),
onClick = onConnect, onClick = {
haptic.confirm()
onConnect()
},
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
enabled = enabled, enabled = enabled,
colors = ButtonDefaults.textButtonColorsPrimary(), colors = ButtonDefaults.textButtonColorsPrimary(),

View File

@@ -2,6 +2,7 @@ package io.github.miuzarte.scrcpyforandroid.widgets
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import io.github.miuzarte.scrcpyforandroid.R import io.github.miuzarte.scrcpyforandroid.R
@@ -9,6 +10,7 @@ import io.github.miuzarte.scrcpyforandroid.pages.LocalRootNavigator
import io.github.miuzarte.scrcpyforandroid.pages.RootScreen import io.github.miuzarte.scrcpyforandroid.pages.RootScreen
import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions.RecordFormat import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions.RecordFormat
import io.github.miuzarte.scrcpyforandroid.services.NativeRecordingSupport import io.github.miuzarte.scrcpyforandroid.services.NativeRecordingSupport
import io.github.miuzarte.scrcpyforandroid.ui.contextClick
import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.Text
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
@@ -23,6 +25,8 @@ fun RecordPreferences(
enabled: Boolean, enabled: Boolean,
onRecordFormatChange: (String) -> Unit, onRecordFormatChange: (String) -> Unit,
) { ) {
val haptic = LocalHapticFeedback.current
val navigator = LocalRootNavigator.current val navigator = LocalRootNavigator.current
val supportedFormats = remember { NativeRecordingSupport.supportedFormats } val supportedFormats = remember { NativeRecordingSupport.supportedFormats }
@@ -43,6 +47,7 @@ fun RecordPreferences(
summary = "--record", summary = "--record",
enabled = enabled, enabled = enabled,
onClick = { onClick = {
haptic.contextClick()
navigator.push(RootScreen.ScrcpyOptionRecord(profileId)) navigator.push(RootScreen.ScrcpyOptionRecord(profileId))
}, },
endActions = { endActions = {