From 7f256882a4d451f8be70ced2817d282d668dc5bb Mon Sep 17 00:00:00 2001 From: Miuzarte <982809597@qq.com> Date: Sun, 31 May 2026 17:47:03 +0800 Subject: [PATCH] improve: complete haptic feedback --- .../LockscreenPasswordActivity.kt | 263 ++++++++-------- .../scrcpyforandroid/pages/AboutScreen.kt | 5 + .../scrcpyforandroid/pages/DeviceTabScreen.kt | 81 ++--- .../pages/FileManagerScreen.kt | 200 ++++++------ .../pages/ScrcpyAllOptionsScreen.kt | 287 ++++++++---------- .../scrcpyforandroid/pages/SettingsScreen.kt | 202 ++++++------ .../scrcpyforandroid/scaffolds/ArrowSlider.kt | 3 +- .../scrcpyforandroid/widgets/DeviceWidgets.kt | 215 +++++++------ .../widgets/RecordPreferences.kt | 5 + 9 files changed, 597 insertions(+), 664 deletions(-) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/LockscreenPasswordActivity.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/LockscreenPasswordActivity.kt index f1720f6..df9ff80 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/LockscreenPasswordActivity.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/LockscreenPasswordActivity.kt @@ -6,14 +6,7 @@ import android.os.Bundle import android.view.WindowManager import androidx.activity.compose.setContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Arrangement -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.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions 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.Edit import androidx.compose.material.icons.rounded.Password -import androidx.compose.runtime.Composable -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.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -45,11 +29,7 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing -import io.github.miuzarte.scrcpyforandroid.password.BiometricGate -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.password.* import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.ReorderableList 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.storage.Settings 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.rememberBlurBackdrop -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -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 kotlinx.coroutines.* +import top.yukonga.miuix.kmp.basic.* import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.icon.MiuixIcons 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.textStyles -class LockscreenPasswordActivity : FragmentActivity() { +class LockscreenPasswordActivity: FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) @@ -154,6 +118,8 @@ private fun LockscreenPasswordScreen( val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val scrollBehavior = MiuixScrollBehavior() + val haptic = LocalHapticFeedback.current + val asBundleShared by appSettings.bundleState.collectAsState() val asBundleSharedLatest by rememberUpdatedState(asBundleShared) var asBundle by rememberSaveable(asBundleShared) { mutableStateOf(asBundleShared) } @@ -232,10 +198,15 @@ private fun LockscreenPasswordScreen( if (blurBackdrop != null) Color.Transparent else colorScheme.surface, navigationIcon = { - IconButton(onClick = { activity.onBackPressedDispatcher.onBackPressed() }) { + IconButton( + onClick = { + haptic.contextClick() + activity.onBackPressedDispatcher.onBackPressed() + }, + ) { Icon( 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, onClick = { pendingCreate = true - } - ) - ) + }, + ), + ), ), ) { Icon( @@ -300,80 +271,86 @@ private fun LockscreenPasswordScreen( }, ) - if (showRiskDialog) { - OverlayDialog( - show = true, - title = stringResource(R.string.password_no_lock_screen), - summary = stringResource(R.string.password_no_lock_screen_warn), - defaultWindowInsetsPadding = false, - onDismissRequest = activity::finish, - ) { - Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal)) { - TextButton( - text = stringResource(R.string.button_cancel), - onClick = activity::finish, - modifier = Modifier.weight(1f), - ) - TextButton( - text = stringResource(R.string.password_agree), - onClick = { showRiskDialog = false }, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.textButtonColorsPrimary(), - ) - } + OverlayDialog( + show = showRiskDialog, + title = stringResource(R.string.password_no_lock_screen), + summary = stringResource(R.string.password_no_lock_screen_warn), + defaultWindowInsetsPadding = false, + onDismissRequest = activity::finish, + ) { + Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal)) { + TextButton( + text = stringResource(R.string.button_cancel), + onClick = { + haptic.contextClick() + activity.finish() + }, + modifier = Modifier.weight(1f), + ) + TextButton( + text = stringResource(R.string.password_agree), + onClick = { + haptic.confirm() + showRiskDialog = false + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary(), + ) } } - if (showDisableDialog) { - OverlayDialog( - show = true, - title = stringResource(R.string.password_auth_lost_warn), - summary = stringResource(R.string.password_auth_lost_detail), - defaultWindowInsetsPadding = false, - onDismissRequest = { showDisableDialog = false }, - ) { - Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal)) { - TextButton( - text = stringResource(R.string.button_cancel), - onClick = { showDisableDialog = false }, - modifier = Modifier.weight(1f), - ) - val textAuthToDisable = stringResource(R.string.password_auth_to_disable) - val textAuthSubtitle = stringResource(R.string.password_auth_subtitle) - TextButton( - text = stringResource(R.string.password_continue_disable), - onClick = { - scope.launch { - if (entries.any { it.cipherText != null }) { - val ok = BiometricGate.authenticate( - activity = activity, - title = textAuthToDisable, - subtitle = textAuthSubtitle, - ) - if (!ok) { - AppRuntime.snackbar(R.string.password_auth_failed) - showDisableDialog = false - return@launch - } + OverlayDialog( + show = showDisableDialog, + title = stringResource(R.string.password_auth_lost_warn), + summary = stringResource(R.string.password_auth_lost_detail), + defaultWindowInsetsPadding = false, + onDismissRequest = { showDisableDialog = false }, + ) { + Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal)) { + TextButton( + text = stringResource(R.string.button_cancel), + onClick = { + haptic.contextClick() + showDisableDialog = false + }, + modifier = Modifier.weight(1f), + ) + val textAuthToDisable = stringResource(R.string.password_auth_to_disable) + val textAuthSubtitle = stringResource(R.string.password_auth_subtitle) + TextButton( + text = stringResource(R.string.password_continue_disable), + onClick = { + haptic.confirm() + scope.launch { + if (entries.any { it.cipherText != null }) { + val ok = BiometricGate.authenticate( + activity = activity, + title = textAuthToDisable, + subtitle = textAuthSubtitle, + ) + 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 } - }, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.textButtonColorsPrimary(), - ) - } + 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 + } + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary(), + ) } } @@ -390,12 +367,16 @@ private fun LockscreenPasswordScreen( Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal)) { TextButton( text = stringResource(R.string.button_cancel), - onClick = { pendingDeleteId = null }, + onClick = { + haptic.contextClick() + pendingDeleteId = null + }, modifier = Modifier.weight(1f), ) TextButton( text = stringResource(R.string.button_delete), onClick = { + haptic.confirm() PasswordRepository.delete(target.id) pendingDeleteId = null }, @@ -467,6 +448,8 @@ private fun LockscreenPasswordPage( onDelete: (PasswordEntry) -> Unit, onMove: (Int, Int) -> Unit, ) { + val haptic = LocalHapticFeedback.current + LazyColumn( contentPadding = contentPadding, scrollBehavior = scrollBehavior, @@ -478,7 +461,7 @@ private fun LockscreenPasswordPage( title = stringResource(R.string.password_require_auth), summary = stringResource( if (canAuthenticate) R.string.password_require_auth_detail - else R.string.password_no_auth_capability + else R.string.password_no_auth_capability, ), checked = requireAuth, enabled = canAuthenticate || requireAuth, @@ -494,7 +477,10 @@ private fun LockscreenPasswordPage( ArrowPreference( title = stringResource(R.string.password_create_new), 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.AuthenticatedCreatedModified -> textBurned }, - onClick = { if (entry.cipherText != null) onRename(entry) }, + onClick = { + haptic.contextClick() + if (entry.cipherText != null) onRename(entry) + }, endActions = listOf( ReorderableList.EndAction.Icon( icon = Icons.Rounded.Edit, contentDescription = textEdit, - onClick = { if (entry.cipherText != null) onRename(entry) }, + onClick = { + haptic.contextClick() + if (entry.cipherText != null) onRename(entry) + }, ), ReorderableList.EndAction.Icon( icon = Icons.Rounded.DeleteOutline, contentDescription = textConfirm, - onClick = { onDelete(entry) }, + onClick = { + haptic.contextClick() + onDelete(entry) + }, ), ), ) @@ -563,6 +558,8 @@ private fun PasswordEditorSheet( onDismissRequest: () -> Unit, onConfirm: (String, String) -> Unit, ) { + val haptic = LocalHapticFeedback.current + val focusManager = LocalFocusManager.current var nameBuffer by rememberSaveable(mode, show, initialName) { mutableStateOf(initialName) } var passwordBuffer by rememberSaveable(mode, show) { mutableStateOf("") } @@ -571,12 +568,17 @@ private fun PasswordEditorSheet( show = show, title = stringResource( if (mode == PasswordDialogMode.Create) R.string.password_create_new - else R.string.password_rename + else R.string.password_rename, ), defaultWindowInsetsPadding = false, onDismissRequest = onDismissRequest, startAction = { - IconButton(onClick = onDismissRequest) { + IconButton( + onClick = { + haptic.contextClick() + onDismissRequest() + }, + ) { Icon( imageVector = MiuixIcons.Close, contentDescription = stringResource(R.string.cd_close), @@ -584,7 +586,12 @@ private fun PasswordEditorSheet( } }, endAction = { - IconButton(onClick = { onConfirm(nameBuffer, passwordBuffer) }) { + IconButton( + onClick = { + haptic.contextClick() + onConfirm(nameBuffer, passwordBuffer) + }, + ) { Icon( imageVector = MiuixIcons.Ok, contentDescription = stringResource(R.string.cd_save), diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt index d938502..de01483 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/AboutScreen.kt @@ -124,6 +124,7 @@ private fun AboutContent( onLogoHeightChanged: (Int) -> Unit, ) { val context = LocalContext.current + val haptic = LocalHapticFeedback.current val updateState by AppUpdateChecker.state.collectAsState() val backdrop = rememberLayerBackdrop() var isOs3Effect by remember { mutableStateOf(true) } @@ -363,6 +364,7 @@ private fun AboutContent( ) }, onClick = { + haptic.contextClick() context.startActivity( Intent(Intent.ACTION_VIEW, AppUpdateChecker.REPO_URL.toUri()), ) @@ -380,6 +382,7 @@ private fun AboutContent( } }, onClick = { + haptic.contextClick() context.startActivity( Intent( Intent.ACTION_VIEW, @@ -400,6 +403,7 @@ private fun AboutContent( ) }, onClick = { + haptic.contextClick() context.startActivity( Intent( Intent.ACTION_VIEW, @@ -424,6 +428,7 @@ private fun AboutContent( ) }, onClick = { + haptic.contextClick() context.startActivity( Intent( Intent.ACTION_VIEW, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt index a842367..f31ca3e 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt @@ -1,36 +1,15 @@ package io.github.miuzarte.scrcpyforandroid.pages import androidx.activity.compose.LocalActivity -import androidx.compose.foundation.layout.Arrangement -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.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.SwapHoriz import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass -import androidx.compose.runtime.Composable -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.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.SectionSmallTitle import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions -import io.github.miuzarte.scrcpyforandroid.services.AppRuntime -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.services.* import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur import io.github.miuzarte.scrcpyforandroid.ui.contextClick import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop -import io.github.miuzarte.scrcpyforandroid.widgets.AppListBottomSheet -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 io.github.miuzarte.scrcpyforandroid.widgets.* import kotlinx.coroutines.Dispatchers 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.SmallTopAppBar -import top.yukonga.miuix.kmp.basic.TextField -import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.basic.* import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.More @@ -114,6 +70,7 @@ internal fun DeviceTabScreen( val viewModel: DeviceTabViewModel = viewModel(factory = viewModelFactory) val navigator = LocalRootNavigator.current + val haptic = LocalHapticFeedback.current var useCompactTopAppBar by remember { mutableStateOf(false) } var showTwoPaneSideAction by remember { mutableStateOf(false) } var configPanelOnLeft by remember { mutableStateOf(true) } @@ -130,13 +87,16 @@ internal fun DeviceTabScreen( val topAppBarActions: @Composable RowScope.() -> Unit = { if (showTwoPaneSideAction) { IconButton( - onClick = { twoPaneSideToggleRequest++ }, + onClick = { + haptic.contextClick() + twoPaneSideToggleRequest++ + }, ) { Icon( imageVector = Icons.Rounded.SwapHoriz, contentDescription = stringResource( 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 = { EventLogger.clearLogs() }, - ) - ) - ) + ), + ), + ), ) { Icon( imageVector = MiuixIcons.More, @@ -175,13 +135,13 @@ internal fun DeviceTabScreen( if (useCompactTopAppBar) SmallTopAppBar( title = stringResource(R.string.device_title), color = topAppBarColor, - actions = topAppBarActions + actions = topAppBarActions, ) else TopAppBar( title = stringResource(R.string.device_title), color = topAppBarColor, actions = topAppBarActions, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, ) } }, @@ -286,7 +246,7 @@ internal fun DeviceTabPage( DisposableEffect(lifecycleOwner) { viewModel.setAppInForeground( - lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) + lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED), ) val observer = LifecycleEventObserver { _, event -> when (event) { @@ -378,7 +338,7 @@ internal fun DeviceTabPage( } else { viewModel.setEditingDeviceId( if (editingDeviceId != device.id) device.id - else null + else null, ) } }, @@ -476,7 +436,6 @@ internal fun DeviceTabPage( } }, onOpenAdvanced = { navigator.push(RootScreen.Advanced) }, - onStartStopHaptic = haptic::contextClick, onStart = viewModel::startScrcpy, onStop = viewModel::stopScrcpy, sessionInfo = sessionInfo, @@ -588,7 +547,6 @@ internal fun DeviceTabPage( } }, onOpenAdvanced = { navigator.push(RootScreen.Advanced) }, - onStartStopHaptic = haptic::contextClick, onStart = viewModel::startScrcpy, onStop = viewModel::stopScrcpy, sessionInfo = sessionInfo, @@ -715,7 +673,7 @@ internal fun DeviceTabPage( .padding(contentPadding) .padding( horizontal = UiSpacing.PageHorizontal, - vertical = UiSpacing.PageVertical + vertical = UiSpacing.PageVertical, ), horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem), ) { @@ -791,6 +749,7 @@ internal fun DeviceTabPage( summary = if (app?.label != null) task.packageName else null, system = app?.system, onClick = { + haptic.contextClick() viewModel.hideRecentTasks() if (sessionInfo == null) viewModel.startScrcpy(task.packageName) else viewModel.launchAppWithFallback(task.packageName) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerScreen.kt index f3955df..49c6225 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FileManagerScreen.kt @@ -3,42 +3,22 @@ package io.github.miuzarte.scrcpyforandroid.pages import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -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.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile -import androidx.compose.material.icons.rounded.Download -import androidx.compose.material.icons.rounded.Folder -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.material.icons.rounded.* +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource 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.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn -import io.github.miuzarte.scrcpyforandroid.services.AppRuntime -import io.github.miuzarte.scrcpyforandroid.services.DirectoryDownloadSnapshot -import io.github.miuzarte.scrcpyforandroid.services.FileManagerService -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 io.github.miuzarte.scrcpyforandroid.services.* +import io.github.miuzarte.scrcpyforandroid.ui.* +import top.yukonga.miuix.kmp.basic.* import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.More @@ -90,6 +51,7 @@ fun FileManagerScreen( onNavigateUpActionChange: (((() -> Boolean)?) -> Unit)? = null, ) { val viewModel: FileManagerViewModel = viewModel() + val haptic = LocalHapticFeedback.current val context = LocalContext.current val blurBackdrop = rememberBlurBackdrop(LocalEnableBlur.current) val blurActive = blurBackdrop != null @@ -178,7 +140,10 @@ fun FileManagerScreen( else colorScheme.surface, navigationIcon = { IconButton( - onClick = viewModel::navigateUp, + onClick = { + haptic.contextClick() + viewModel.navigateUp() + }, enabled = pathStack.size > 1, ) { Icon( @@ -194,10 +159,12 @@ fun FileManagerScreen( .fillMaxWidth() .combinedClickable( onClick = { + haptic.contextClick() pathInput = currentPath showPathDialog = true }, onLongClick = { + // 自带 haptic pathInput = currentPath showPathDialog = true }, @@ -244,11 +211,11 @@ fun FileManagerScreen( 2 -> FileManagerSortField.TIME 3 -> FileManagerSortField.EXTENSION else -> FileManagerSortField.NAME - } + }, ) }, ) - } + }, ), DropdownEntry( items = dirOptions.mapIndexed { i, option -> @@ -257,13 +224,13 @@ fun FileManagerScreen( selected = i == dirIdx, onClick = { viewModel.updateSort( - descending = i == 1 + descending = i == 1, ) }, ) - } + }, ), - ) + ), ) { Icon( imageVector = MiuixIcons.Tune, @@ -286,9 +253,9 @@ fun FileManagerScreen( onClick = { uploadLauncher.launch(arrayOf("*/*")) }, - ) - ) - ) + ), + ), + ), ) { Icon( imageVector = MiuixIcons.More, @@ -303,7 +270,7 @@ fun FileManagerScreen( Box( modifier = if (blurActive) Modifier.layerBackdrop(blurBackdrop) - else Modifier + else Modifier, ) { FileManagerPage( contentPadding = pagePadding, @@ -320,7 +287,7 @@ fun FileManagerScreen( viewModel.saveScrollPosition( currentPath, listState.firstVisibleItemIndex, - listState.firstVisibleItemScrollOffset + listState.firstVisibleItemScrollOffset, ) viewModel.openEntry(entry) }, @@ -356,30 +323,28 @@ fun FileManagerScreen( && (!entry.isDirectory || selectedSnapshot != null), ) } - if (showPathDialog) { - PathJumpDialog( - show = showPathDialog, - path = pathInput, - onPathChange = { pathInput = it }, - onDismissRequest = { showPathDialog = false }, - onConfirm = { - showPathDialog = false - viewModel.jumpToPath(pathInput) - }, - ) - } - if (showCreateFolderDialog) { - CreateFolderDialog( - show = showCreateFolderDialog, - folderName = newFolderName, - onFolderNameChange = { newFolderName = it }, - onDismissRequest = { showCreateFolderDialog = false }, - onConfirm = { - showCreateFolderDialog = false - viewModel.createFolder(newFolderName) - }, - ) - } + + PathJumpDialog( + show = showPathDialog, + path = pathInput, + onPathChange = { pathInput = it }, + onDismissRequest = { showPathDialog = false }, + onConfirm = { + showPathDialog = false + viewModel.jumpToPath(pathInput) + }, + ) + + CreateFolderDialog( + show = showCreateFolderDialog, + folderName = newFolderName, + onFolderNameChange = { newFolderName = it }, + onDismissRequest = { showCreateFolderDialog = false }, + onConfirm = { + showCreateFolderDialog = false + viewModel.createFolder(newFolderName) + }, + ) } @Composable @@ -430,17 +395,17 @@ private fun FileManagerPage( when { loading -> FileManagerStatusCard( message = stringResource(R.string.text_loading), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) errorText != null -> FileManagerStatusCard( message = stringResource(R.string.fm_load_failed, errorText), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) displayedEntries.isEmpty() -> FileManagerStatusCard( message = stringResource(R.string.fm_empty_dir), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } } @@ -457,7 +422,7 @@ private fun FileManagerPage( items(fileRows) { rowEntries -> Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem) + horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem), ) { rowEntries.forEach { entry -> FileManagerItemCard( @@ -503,10 +468,15 @@ private fun FileManagerItemCard( onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { + val haptic = LocalHapticFeedback.current + Card( modifier = modifier .combinedClickable( - onClick = onClick, + onClick = { + haptic.contextClick() + onClick() + }, onLongClick = onLongClick, ), ) { @@ -560,6 +530,8 @@ private fun FileDetailsBottomSheet( onDownload: () -> Unit, downloadEnabled: Boolean, ) { + val haptic = LocalHapticFeedback.current + OverlayBottomSheet( show = show, title = stringResource(R.string.fm_file_details), @@ -567,7 +539,10 @@ private fun FileDetailsBottomSheet( onDismissFinished = onDismissFinished, startAction = { IconButton( - onClick = onToggleRaw + onClick = { + haptic.contextClick() + onToggleRaw() + }, ) { Icon( imageVector = @@ -575,14 +550,17 @@ private fun FileDetailsBottomSheet( else Icons.Rounded.RawOn, contentDescription = stringResource( if (!showingRaw) R.string.fm_show_raw - else R.string.fm_show_parsed + else R.string.fm_show_parsed, ), ) } }, endAction = { IconButton( - onClick = onDownload, + onClick = { + haptic.contextClick() + onDownload() + }, enabled = downloadEnabled, ) { Icon( @@ -595,7 +573,7 @@ private fun FileDetailsBottomSheet( LazyColumn( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(2f / 3f) + .fillMaxHeight(2f / 3f), ) { item { TextField( @@ -618,6 +596,8 @@ private fun PathJumpDialog( onDismissRequest: () -> Unit, onConfirm: () -> Unit, ) { + val haptic = LocalHapticFeedback.current + OverlayDialog( show = show, title = stringResource(R.string.fm_goto_path), @@ -628,17 +608,26 @@ private fun PathJumpDialog( TextField( value = path, onValueChange = onPathChange, - label = "/storage/emulated/0", - useLabelAsPlaceholder = true, + // label = "/storage/emulated/0", + // useLabelAsPlaceholder = true, ) - Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem), + ) { TextButton( text = stringResource(R.string.button_cancel), - onClick = onDismissRequest, + onClick = { + haptic.contextClick() + onDismissRequest() + }, ) TextButton( text = stringResource(R.string.button_confirm), - onClick = onConfirm, + onClick = { + haptic.confirm() + onConfirm() + }, ) } } @@ -653,6 +642,8 @@ private fun CreateFolderDialog( onDismissRequest: () -> Unit, onConfirm: () -> Unit, ) { + val haptic = LocalHapticFeedback.current + OverlayDialog( show = show, title = stringResource(R.string.fm_title_create_folder), @@ -666,14 +657,23 @@ private fun CreateFolderDialog( label = stringResource(R.string.fm_label_new_folder), useLabelAsPlaceholder = true, ) - Row(horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(UiSpacing.PageItem), + ) { TextButton( text = stringResource(R.string.button_cancel), - onClick = onDismissRequest, + onClick = { + haptic.contextClick() + onDismissRequest() + }, ) TextButton( text = stringResource(R.string.fm_button_create), - onClick = onConfirm, + onClick = { + haptic.confirm() + onConfirm() + }, ) } } @@ -695,13 +695,13 @@ private fun buildDetailsText( ): String { val details = StringBuilder( if (showRaw) stat.rawOutput - else FileManagerService.formatStatDetails(stat, directorySnapshot) + else FileManagerService.formatStatDetails(stat, directorySnapshot), ) if (targetStat != null) { details.append("\n\n${AppRuntime.stringResource(R.string.fm_stat_target_info)}\n") details.append( if (showRaw) targetStat.rawOutput - else FileManagerService.formatStatDetails(targetStat) + else FileManagerService.formatStatDetails(targetStat), ) } return details.toString() diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt index 272ce8b..3e52d13 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/ScrcpyAllOptionsScreen.kt @@ -1,17 +1,7 @@ package io.github.miuzarte.scrcpyforandroid.pages import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Arrangement -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.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.text.KeyboardActions 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.DeleteOutline import androidx.compose.material.icons.rounded.Edit -import androidx.compose.runtime.Composable -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.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource 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.ScrcpyOptions.Crop import io.github.miuzarte.scrcpyforandroid.models.ScrcpyOptions.NewDisplay -import io.github.miuzarte.scrcpyforandroid.scaffolds.LazyColumn -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.scaffolds.* import io.github.miuzarte.scrcpyforandroid.scrcpy.ClientOptions import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy -import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.AudioSource -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.scrcpy.Shared.* import io.github.miuzarte.scrcpyforandroid.services.AppRuntime import io.github.miuzarte.scrcpyforandroid.storage.ScrcpyOptions 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.scrcpyOptions import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyProfiles -import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar -import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur -import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop +import io.github.miuzarte.scrcpyforandroid.ui.* import io.github.miuzarte.scrcpyforandroid.widgets.RecordPreferences -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.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 kotlinx.coroutines.* +import top.yukonga.miuix.kmp.basic.* import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.More @@ -142,7 +92,7 @@ internal fun ScrcpyAllOptionsScreen( soBundleShared else scrcpyProfilesState.profiles .firstOrNull { it.id == selectedProfileId } - ?.bundle ?: soBundleShared + ?.bundle ?: soBundleShared, ) } val lastValidSoBundleState = rememberSaveable(selectedProfileId) { @@ -186,7 +136,7 @@ internal fun ScrcpyAllOptionsScreen( if (device.scrcpyProfileId == profileId) device.copy(scrcpyProfileId = ScrcpyOptions.GLOBAL_PROFILE_ID) else device - } + }, ) if (updated != shortcuts) { quickDevices.updateBundle { bundle -> @@ -235,9 +185,9 @@ internal fun ScrcpyAllOptionsScreen( onClick = { showManageProfilesSheet = true }, - ) - ) - ) + ), + ), + ), ) { Icon( imageVector = MiuixIcons.More, @@ -277,7 +227,7 @@ internal fun ScrcpyAllOptionsScreen( height = 48.dp, itemSpacing = UiSpacing.Medium, ) - } + }, ) } }, @@ -286,7 +236,7 @@ internal fun ScrcpyAllOptionsScreen( Box( modifier = if (blurActive) Modifier.layerBackdrop(blurBackdrop) - else Modifier + else Modifier, ) { ScrcpyAllOptionsPage( contentPadding = contentPadding, @@ -606,7 +556,7 @@ internal fun ScrcpyAllOptionsPage( val screenOffTimeoutPresetIndex = rememberSaveable(soBundle.screenOffTimeout) { 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( title = info.id, summary = info.type.s, - ) + ), ) } } @@ -702,7 +652,7 @@ internal fun ScrcpyAllOptionsPage( SpinnerEntry( title = info.id, summary = info.type.s, - ) + ), ) } } @@ -760,7 +710,7 @@ internal fun ScrcpyAllOptionsPage( }, title = app.label?.takeIf { it.isNotBlank() } ?: app.packageName, summary = app.packageName, - ) + ), ) } } @@ -893,13 +843,13 @@ internal fun ScrcpyAllOptionsPage( checked = soBundle.turnScreenOff, onCheckedChange = { soBundle = soBundle.copy( - turnScreenOff = it + turnScreenOff = it, ) if (it) AppRuntime.snackbar( // github.com/Genymobile/scrcpy/issues/3376 // github.com/Genymobile/scrcpy/issues/4587 // 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, onCheckedChange = { soBundle = soBundle.copy( - control = !it + control = !it, ) }, ) @@ -919,7 +869,7 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.video, onCheckedChange = { soBundle = soBundle.copy( - video = !it + video = !it, ) }, ) @@ -929,7 +879,7 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.videoPlayback, onCheckedChange = { soBundle = soBundle.copy( - videoPlayback = !it + videoPlayback = !it, ) }, enabled = soBundle.video, @@ -940,7 +890,7 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.audio, onCheckedChange = { soBundle = soBundle.copy( - audio = !it + audio = !it, ) }, ) @@ -950,7 +900,7 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.audioPlayback, onCheckedChange = { soBundle = soBundle.copy( - audioPlayback = !it + audioPlayback = !it, ) }, enabled = soBundle.audio, @@ -968,7 +918,7 @@ internal fun ScrcpyAllOptionsPage( ?.toLong() ?.let(Tick::fromSec) ?.value - ?: -1 + ?: -1, ) }, valueRange = 0f..ScrcpyPresets.ScreenOffTimeout.lastIndex.toFloat(), @@ -989,7 +939,7 @@ internal fun ScrcpyAllOptionsPage( ?.takeIf { value -> value > 0 } ?.let(Tick::fromSec) ?.value - ?: -1 + ?: -1, ) }, ) @@ -999,7 +949,7 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.powerOn, onCheckedChange = { soBundle = soBundle.copy( - powerOn = !it + powerOn = !it, ) }, ) @@ -1009,7 +959,7 @@ internal fun ScrcpyAllOptionsPage( checked = soBundle.powerOffOnClose, onCheckedChange = { soBundle = soBundle.copy( - powerOffOnClose = it + powerOffOnClose = it, ) }, ) @@ -1019,7 +969,7 @@ internal fun ScrcpyAllOptionsPage( checked = soBundle.stayAwake, onCheckedChange = { soBundle = soBundle.copy( - stayAwake = it + stayAwake = it, ) }, ) @@ -1029,7 +979,7 @@ internal fun ScrcpyAllOptionsPage( checked = soBundle.keepActive, onCheckedChange = { soBundle = soBundle.copy( - keepActive = it + keepActive = it, ) }, ) @@ -1039,7 +989,7 @@ internal fun ScrcpyAllOptionsPage( checked = soBundle.showTouches, onCheckedChange = { soBundle = soBundle.copy( - showTouches = it + showTouches = it, ) }, ) @@ -1049,7 +999,7 @@ internal fun ScrcpyAllOptionsPage( checked = soBundle.fullscreen, onCheckedChange = { soBundle = soBundle.copy( - fullscreen = it + fullscreen = it, ) }, ) @@ -1059,10 +1009,10 @@ internal fun ScrcpyAllOptionsPage( checked = soBundle.disableScreensaver, onCheckedChange = { soBundle = soBundle.copy( - disableScreensaver = it + disableScreensaver = it, ) 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, onCheckedChange = { soBundle = soBundle.copy( - killAdbOnClose = it + killAdbOnClose = it, ) }, ) @@ -1097,7 +1047,7 @@ internal fun ScrcpyAllOptionsPage( selectedIndex = audioCodecIndex, onSelectedIndexChange = { soBundle = soBundle.copy( - audioCodec = Codec.AUDIO[it].string + audioCodec = Codec.AUDIO[it].string, ) }, ) @@ -1111,7 +1061,7 @@ internal fun ScrcpyAllOptionsPage( val idx = value.roundToInt() .coerceIn(0, ScrcpyPresets.AudioBitRate.lastIndex) soBundle = soBundle.copy( - audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000 + audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000, ) }, valueRange = 0f..ScrcpyPresets.AudioBitRate.lastIndex.toFloat(), @@ -1129,7 +1079,7 @@ internal fun ScrcpyAllOptionsPage( ?.takeIf { it >= 0 } ?.let { soBundle = soBundle.copy( - audioBitRate = it * 1000 + audioBitRate = it * 1000, ) } }, @@ -1142,7 +1092,7 @@ internal fun ScrcpyAllOptionsPage( selectedIndex = videoCodecIndex, onSelectedIndexChange = { 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, onValueChange = { mbps -> soBundle = soBundle.copy( - videoBitRate = (mbps * 10).roundToInt() * (1_000_000 / 10) + videoBitRate = (mbps * 10).roundToInt() * (1_000_000 / 10), ) }, valueRange = 0f..40f, @@ -1182,7 +1132,7 @@ internal fun ScrcpyAllOptionsPage( raw.toFloatOrNull()?.let { parsed -> if (parsed >= 0f) { soBundle = soBundle.copy( - videoBitRate = (parsed * 1_000_000f).roundToInt() + videoBitRate = (parsed * 1_000_000f).roundToInt(), ) } } @@ -1200,7 +1150,7 @@ internal fun ScrcpyAllOptionsPage( selectedIndex = videoSourceIndex, onSelectedIndexChange = { soBundle = soBundle.copy( - videoSource = VideoSource.entries[it].string + videoSource = VideoSource.entries[it].string, ) }, ) @@ -1236,7 +1186,7 @@ internal fun ScrcpyAllOptionsPage( soBundle = soBundle.copy( displayId = 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() .coerceIn(0, ScrcpyPresets.MinSizeAlignment.lastIndex) soBundle = soBundle.copy( - minSizeAlignment = ScrcpyPresets.MinSizeAlignment[idx] + minSizeAlignment = ScrcpyPresets.MinSizeAlignment[idx], ) }, valueRange = 0f..ScrcpyPresets.MinSizeAlignment.lastIndex.toFloat(), @@ -1269,7 +1219,7 @@ internal fun ScrcpyAllOptionsPage( return@ArrowSlider } soBundle = soBundle.copy( - minSizeAlignment = parsed + minSizeAlignment = parsed, ) }, ) @@ -1281,7 +1231,7 @@ internal fun ScrcpyAllOptionsPage( val idx = it.roundToInt() .coerceIn(0, ScrcpyPresets.MaxSize.lastIndex) soBundle = soBundle.copy( - maxSize = ScrcpyPresets.MaxSize[idx] + maxSize = ScrcpyPresets.MaxSize[idx], ) }, valueRange = 0f..ScrcpyPresets.MaxSize.lastIndex.toFloat(), @@ -1300,7 +1250,7 @@ internal fun ScrcpyAllOptionsPage( inputValueRange = 0f..UInt.MAX_VALUE.toFloat(), onInputConfirm = { soBundle = soBundle.copy( - maxSize = it.toIntOrNull() ?: run { 0 } + maxSize = it.toIntOrNull() ?: run { 0 }, ) }, ) @@ -1314,7 +1264,7 @@ internal fun ScrcpyAllOptionsPage( soBundle = soBundle.copy( maxFps = if (idx == 0) "" - else ScrcpyPresets.MaxFPS[idx].toString() + else ScrcpyPresets.MaxFPS[idx].toString(), ) }, valueRange = 0f..ScrcpyPresets.MaxFPS.lastIndex.toFloat(), @@ -1330,7 +1280,7 @@ internal fun ScrcpyAllOptionsPage( inputValueRange = 0f..UShort.MAX_VALUE.toFloat(), onInputConfirm = { soBundle = soBundle.copy( - maxFps = it + maxFps = it, ) }, ) @@ -1368,7 +1318,7 @@ internal fun ScrcpyAllOptionsPage( soBundle = soBundle.copy( cameraId = if (it == 0) "" - else cameras[it - 1].id + else cameras[it - 1].id, ) }, ) @@ -1381,7 +1331,7 @@ internal fun ScrcpyAllOptionsPage( soBundle = soBundle.copy( cameraFacing = if (it == 0) "" - else CameraFacing.entries[it].string + else CameraFacing.entries[it].string, ) }, ) @@ -1425,7 +1375,7 @@ internal fun ScrcpyAllOptionsPage( 1 -> { // "自定义" - 进入自定义输入模式 soBundle = soBundle.copy( - cameraSizeUseCustom = true + cameraSizeUseCustom = true, ) cameraSizeCustomInput = "" } @@ -1452,11 +1402,11 @@ internal fun ScrcpyAllOptionsPage( // 输入的值存在于列表中, 取消自定义输入 soBundle.copy( cameraSize = cameraSizeCustomInput, - cameraSizeUseCustom = false + cameraSizeUseCustom = false, ) } else { soBundle.copy( - cameraSizeCustom = cameraSizeCustomInput + cameraSizeCustom = cameraSizeCustomInput, ) } }, @@ -1473,7 +1423,7 @@ internal fun ScrcpyAllOptionsPage( onValueChange = { cameraArInput = it }, onFocusLost = { soBundle = soBundle.copy( - cameraAr = cameraArInput + cameraAr = cameraArInput, ) }, label = "--camera-ar", @@ -1487,7 +1437,7 @@ internal fun ScrcpyAllOptionsPage( onValueChange = { cameraZoomInput = it }, onFocusLost = { soBundle = soBundle.copy( - cameraZoom = cameraZoomInput + cameraZoom = cameraZoomInput, ) }, label = "--camera-zoom", @@ -1504,7 +1454,7 @@ internal fun ScrcpyAllOptionsPage( val idx = value.roundToInt() .coerceIn(0, ScrcpyPresets.CameraFps.lastIndex) soBundle = soBundle.copy( - cameraFps = ScrcpyPresets.CameraFps[idx] + cameraFps = ScrcpyPresets.CameraFps[idx], ) }, valueRange = 0f..ScrcpyPresets.CameraFps.lastIndex.toFloat(), @@ -1521,7 +1471,7 @@ internal fun ScrcpyAllOptionsPage( inputValueRange = 0f..UShort.MAX_VALUE.toFloat(), onInputConfirm = { soBundle = soBundle.copy( - cameraFps = it.toIntOrNull() ?: run { 0 } + cameraFps = it.toIntOrNull() ?: run { 0 }, ) }, ) @@ -1531,7 +1481,7 @@ internal fun ScrcpyAllOptionsPage( checked = soBundle.cameraHighSpeed, onCheckedChange = { soBundle = soBundle.copy( - cameraHighSpeed = it + cameraHighSpeed = it, ) }, ) @@ -1541,7 +1491,7 @@ internal fun ScrcpyAllOptionsPage( checked = soBundle.cameraTorch, onCheckedChange = { soBundle = soBundle.copy( - cameraTorch = it + cameraTorch = it, ) }, ) @@ -1560,7 +1510,7 @@ internal fun ScrcpyAllOptionsPage( selectedIndex = audioSourceIndex, onSelectedIndexChange = { soBundle = soBundle.copy( - audioSource = AudioSource.entries[it].string + audioSource = AudioSource.entries[it].string, ) }, ) @@ -1570,7 +1520,7 @@ internal fun ScrcpyAllOptionsPage( checked = soBundle.audioDup, onCheckedChange = { soBundle = soBundle.copy( - audioDup = it + audioDup = it, ) }, ) @@ -1580,7 +1530,7 @@ internal fun ScrcpyAllOptionsPage( checked = soBundle.requireAudio, onCheckedChange = { soBundle = soBundle.copy( - requireAudio = it + requireAudio = it, ) }, ) @@ -1619,7 +1569,7 @@ internal fun ScrcpyAllOptionsPage( soBundle = soBundle.copy( videoEncoder = if (it == 0) "" - else videoEncoders[it - 1].id + else videoEncoders[it - 1].id, ) }, ) @@ -1628,7 +1578,7 @@ internal fun ScrcpyAllOptionsPage( onValueChange = { videoCodecOptionsInput = it }, onFocusLost = { soBundle = soBundle.copy( - videoCodecOptions = videoCodecOptionsInput + videoCodecOptions = videoCodecOptionsInput, ) }, label = "--video-codec-options", @@ -1667,7 +1617,7 @@ internal fun ScrcpyAllOptionsPage( soBundle = soBundle.copy( audioEncoder = if (it == 0) "" - else audioEncoders[it - 1].id + else audioEncoders[it - 1].id, ) }, ) @@ -1676,7 +1626,7 @@ internal fun ScrcpyAllOptionsPage( onValueChange = { audioCodecOptionsInput = it }, onFocusLost = { soBundle = soBundle.copy( - audioCodecOptions = audioCodecOptionsInput + audioCodecOptions = audioCodecOptionsInput, ) }, label = "--audio-codec-options", @@ -1699,7 +1649,7 @@ internal fun ScrcpyAllOptionsPage( soBundle = soBundle.copy( displayImePolicy = if (it == 0) "" - else DisplayImePolicy.entries[it].string + else DisplayImePolicy.entries[it].string, ) }, ) @@ -1709,7 +1659,7 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.vdDestroyContent, onCheckedChange = { soBundle = soBundle.copy( - vdDestroyContent = !it + vdDestroyContent = !it, ) }, ) @@ -1719,7 +1669,7 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.vdSystemDecorations, onCheckedChange = { soBundle = soBundle.copy( - vdSystemDecorations = !it + vdSystemDecorations = !it, ) }, ) @@ -1729,10 +1679,10 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.downsizeOnError, onCheckedChange = { soBundle = soBundle.copy( - downsizeOnError = !it + downsizeOnError = !it, ) 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, onCheckedChange = { soBundle = soBundle.copy( - legacyPaste = it + legacyPaste = it, ) }, ) @@ -1761,7 +1711,7 @@ internal fun ScrcpyAllOptionsPage( 1 -> ClientOptions.KeyInjectMode.PREFER_TEXT.string 2 -> ClientOptions.KeyInjectMode.RAW.string else -> ClientOptions.KeyInjectMode.MIXED.string - } + }, ) }, ) @@ -1771,7 +1721,7 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.forwardKeyRepeat, onCheckedChange = { soBundle = soBundle.copy( - forwardKeyRepeat = !it + forwardKeyRepeat = !it, ) }, enabled = soBundle.keyInjectMode != ClientOptions.KeyInjectMode.PREFER_TEXT.string, @@ -1782,7 +1732,7 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.clipboardAutosync, onCheckedChange = { soBundle = soBundle.copy( - clipboardAutosync = !it + clipboardAutosync = !it, ) }, ) @@ -1792,7 +1742,7 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.mouseHover, onCheckedChange = { soBundle = soBundle.copy( - mouseHover = !it + mouseHover = !it, ) }, ) @@ -1802,10 +1752,10 @@ internal fun ScrcpyAllOptionsPage( checked = !soBundle.cleanup, onCheckedChange = { soBundle = soBundle.copy( - cleanup = !it + cleanup = !it, ) 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, onCheckedChange = { soBundle = soBundle.copy( - flexDisplay = it + flexDisplay = it, ) if (it) AppRuntime.snackbar( - "untested" + "untested", ) }, ) @@ -1865,7 +1815,7 @@ internal fun ScrcpyAllOptionsPage( 1 -> { soBundle = soBundle.copy( - startAppUseCustom = true + startAppUseCustom = true, ) startAppCustomInput = "" } @@ -1893,7 +1843,7 @@ internal fun ScrcpyAllOptionsPage( ) } else { soBundle.copy( - startAppCustom = startAppCustomInput + startAppCustom = startAppCustomInput, ) } }, @@ -1935,8 +1885,8 @@ internal fun ScrcpyAllOptionsPage( newDisplay = NewDisplay.parseFrom( newDisplayWidthInput, newDisplayHeightInput, - newDisplayDpiInput - ).toString() + newDisplayDpiInput, + ).toString(), ) }, singleLine = true, @@ -1958,8 +1908,8 @@ internal fun ScrcpyAllOptionsPage( newDisplay = NewDisplay.parseFrom( newDisplayWidthInput, newDisplayHeightInput, - newDisplayDpiInput - ).toString() + newDisplayDpiInput, + ).toString(), ) }, singleLine = true, @@ -1981,8 +1931,8 @@ internal fun ScrcpyAllOptionsPage( newDisplay = NewDisplay.parseFrom( newDisplayWidthInput, newDisplayHeightInput, - newDisplayDpiInput - ).toString() + newDisplayDpiInput, + ).toString(), ) }, singleLine = true, @@ -2023,7 +1973,7 @@ internal fun ScrcpyAllOptionsPage( TextButton( text = stringResource( if (newDisplayWidthBlank || newDisplayHeightBlank) R.string.scrcpyopt_native - else R.string.button_swap + else R.string.button_swap, ), onClick = { if (newDisplayWidthBlank || newDisplayHeightBlank) { @@ -2044,8 +1994,8 @@ internal fun ScrcpyAllOptionsPage( newDisplay = NewDisplay.parseFrom( newDisplayWidthInput, newDisplayHeightInput, - newDisplayDpiInput - ).toString() + newDisplayDpiInput, + ).toString(), ) }, modifier = Modifier.width(trailingButtonWidth), @@ -2086,8 +2036,8 @@ internal fun ScrcpyAllOptionsPage( cropWidthInput, cropHeightInput, cropXInput, - cropYInput - ).toString() + cropYInput, + ).toString(), ) }, singleLine = true, @@ -2110,8 +2060,8 @@ internal fun ScrcpyAllOptionsPage( cropWidthInput, cropHeightInput, cropXInput, - cropYInput - ).toString() + cropYInput, + ).toString(), ) }, singleLine = true, @@ -2139,8 +2089,8 @@ internal fun ScrcpyAllOptionsPage( cropWidthInput, cropHeightInput, cropXInput, - cropYInput - ).toString() + cropYInput, + ).toString(), ) }, singleLine = true, @@ -2163,8 +2113,8 @@ internal fun ScrcpyAllOptionsPage( cropWidthInput, cropHeightInput, cropXInput, - cropYInput - ).toString() + cropYInput, + ).toString(), ) }, singleLine = true, @@ -2193,7 +2143,7 @@ internal fun ScrcpyAllOptionsPage( selectedIndex = logLevelIndex, onSelectedIndexChange = { soBundle = soBundle.copy( - logLevel = LogLevel.entries[it].string + logLevel = LogLevel.entries[it].string, ) }, ) @@ -2218,6 +2168,8 @@ private fun ProfileNameDialog( onConfirm: (String, String?) -> Unit, ) { if (mode == null) return + + val haptic = LocalHapticFeedback.current val focusManager = LocalFocusManager.current val textDefault = stringResource(R.string.text_default) @@ -2282,12 +2234,18 @@ private fun ProfileNameDialog( ) { TextButton( text = stringResource(R.string.button_cancel), - onClick = onDismissRequest, + onClick = { + haptic.contextClick() + onDismissRequest() + }, modifier = Modifier.weight(1f), ) TextButton( text = stringResource(R.string.button_confirm), - onClick = { onConfirm(input, copySourceProfileId) }, + onClick = { + haptic.confirm() + onConfirm(input, copySourceProfileId) + }, modifier = Modifier.weight(1f), colors = ButtonDefaults.textButtonColorsPrimary(), ) @@ -2345,14 +2303,14 @@ private fun ManageProfilesSheet( icon = Icons.Rounded.Edit, contentDescription = textRename, onClick = { onRenameProfile(profile.id) }, - ) + ), ) add( ReorderableList.EndAction.Icon( icon = Icons.Rounded.DeleteOutline, contentDescription = textDelete, onClick = { onDeleteProfile(profile.id) }, - ) + ), ) } }, @@ -2372,9 +2330,10 @@ private fun DeleteProfileDialog( onDismissRequest: () -> Unit, onConfirm: () -> Unit, ) { - if (!show) return + val haptic = LocalHapticFeedback.current + OverlayDialog( - show = true, + show = show, title = stringResource(R.string.scrcpyopt_delete_profile), summary = stringResource(R.string.scrcpyopt_delete_confirm, profileName), defaultWindowInsetsPadding = false, @@ -2385,12 +2344,18 @@ private fun DeleteProfileDialog( ) { TextButton( text = stringResource(R.string.button_cancel), - onClick = onDismissRequest, + onClick = { + haptic.contextClick() + onDismissRequest() + }, modifier = Modifier.weight(1f), ) TextButton( text = stringResource(R.string.button_delete), - onClick = onConfirm, + onClick = { + haptic.confirm() + onConfirm() + }, modifier = Modifier.weight(1f), colors = ButtonDefaults.textButtonColorsPrimary(), ) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt index 0ba8e36..ef127aa 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt @@ -9,32 +9,18 @@ import androidx.activity.compose.LocalActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Clear import androidx.compose.material.icons.rounded.FileOpen import androidx.compose.material.icons.rounded.Refresh -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.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight 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.constants.UiSpacing 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.SectionSmallTitle -import io.github.miuzarte.scrcpyforandroid.scaffolds.ArrowSlider import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy 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.Storage.adbClientData import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings -import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar -import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur -import io.github.miuzarte.scrcpyforandroid.ui.MonetKeyColorOptions -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 io.github.miuzarte.scrcpyforandroid.ui.* +import kotlinx.coroutines.* +import top.yukonga.miuix.kmp.basic.* import top.yukonga.miuix.kmp.blur.layerBackdrop import top.yukonga.miuix.kmp.preference.ArrowPreference import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference @@ -172,6 +141,7 @@ fun SettingsPage( val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val scope = rememberCoroutineScope() + val haptic = LocalHapticFeedback.current val navigator = LocalRootNavigator.current val serverPicker = LocalServerPicker.current val terminalFontPicker = LocalTerminalFontPicker.current @@ -215,19 +185,19 @@ fun SettingsPage( var serverRemotePathInput by rememberSaveable(asBundle.serverRemotePath) { mutableStateOf( if (asBundle.serverRemotePath == AppSettings.SERVER_REMOTE_PATH.defaultValue) "" - else asBundle.serverRemotePath + else asBundle.serverRemotePath, ) } var adbKeyNameInput by rememberSaveable(asBundle.adbKeyName) { mutableStateOf( if (asBundle.adbKeyName == AppSettings.ADB_KEY_NAME.defaultValue) "" - else asBundle.adbKeyName + else asBundle.adbKeyName, ) } val adbPrivateKeyPicker = rememberLauncherForActivityResult( - ActivityResultContracts.OpenDocument() + ActivityResultContracts.OpenDocument(), ) { uri: Uri? -> if (uri == null) return@rememberLauncherForActivityResult scope.launch { @@ -247,7 +217,7 @@ fun SettingsPage( } } val adbPublicKeyPicker = rememberLauncherForActivityResult( - ActivityResultContracts.OpenDocument() + ActivityResultContracts.OpenDocument(), ) { uri: Uri? -> if (uri == null) return@rememberLauncherForActivityResult scope.launch { @@ -310,7 +280,7 @@ fun SettingsPage( selected = lang.second == asBundle.languageTag, onClick = { asBundle = asBundle.copy( - languageTag = lang.second + languageTag = lang.second, ) MainActivity.setAppLanguageTag(context, lang.second) activity?.recreate() @@ -328,7 +298,7 @@ fun SettingsPage( .coerceIn(0, AppSettings.ThemeModes.baseOptions.lastIndex), onSelectedIndexChange = { asBundle = asBundle.copy( - themeBaseIndex = it + themeBaseIndex = it, ) }, ) @@ -338,7 +308,7 @@ fun SettingsPage( checked = asBundle.monet, onCheckedChange = { asBundle = asBundle.copy( - monet = it + monet = it, ) }, ) @@ -352,7 +322,7 @@ fun SettingsPage( .coerceIn(0, MonetKeyColorOptions.lastIndex), onSelectedIndexChange = { asBundle = asBundle.copy( - monetSeedIndex = it + monetSeedIndex = it, ) }, ) @@ -368,7 +338,7 @@ fun SettingsPage( .coerceIn(0, monetPaletteStyleOptions.lastIndex), onSelectedIndexChange = { asBundle = asBundle.copy( - monetPaletteStyle = it + monetPaletteStyle = it, ) }, ) @@ -380,7 +350,7 @@ fun SettingsPage( .coerceIn(0, monetColorSpecOptions.lastIndex), onSelectedIndexChange = { asBundle = asBundle.copy( - monetColorSpec = it + monetColorSpec = it, ) }, ) @@ -392,9 +362,9 @@ fun SettingsPage( checked = asBundle.blur, onCheckedChange = { asBundle = asBundle.copy( - blur = it + blur = it, ) - } + }, ) SwitchPreference( title = stringResource(R.string.pref_title_floating_bottom_bar), @@ -402,9 +372,9 @@ fun SettingsPage( checked = asBundle.floatingBottomBar, onCheckedChange = { asBundle = asBundle.copy( - floatingBottomBar = it + floatingBottomBar = it, ) - } + }, ) AnimatedVisibility(asBundle.floatingBottomBar && asBundle.blur) { Column { @@ -415,9 +385,9 @@ fun SettingsPage( && asBundle.floatingBottomBarBlur, onCheckedChange = { asBundle = asBundle.copy( - floatingBottomBarBlur = it + floatingBottomBarBlur = it, ) - } + }, ) } } @@ -435,7 +405,7 @@ fun SettingsPage( onCheckedChange = { if (!isScrcpyStreaming) asBundle = asBundle.copy( - lowLatency = it + lowLatency = it, ) }, ) @@ -445,7 +415,7 @@ fun SettingsPage( checked = asBundle.fullscreenDebugInfo, onCheckedChange = { asBundle = asBundle.copy( - fullscreenDebugInfo = it + fullscreenDebugInfo = it, ) }, ) @@ -455,7 +425,7 @@ fun SettingsPage( checked = asBundle.hideSimpleConfigItems, onCheckedChange = { asBundle = asBundle.copy( - hideSimpleConfigItems = it + hideSimpleConfigItems = it, ) }, ) @@ -466,7 +436,7 @@ fun SettingsPage( onValueChange = { asBundle = asBundle.copy( devicePreviewCardHeightDp = - it.roundToInt().coerceAtLeast(120) + it.roundToInt().coerceAtLeast(120), ) }, valueRange = 160f..600f, @@ -479,7 +449,7 @@ fun SettingsPage( onInputConfirm = { input -> input.toIntOrNull()?.let { asBundle = asBundle.copy( - devicePreviewCardHeightDp = it.coerceAtLeast(120) + devicePreviewCardHeightDp = it.coerceAtLeast(120), ) } }, @@ -487,17 +457,24 @@ fun SettingsPage( ArrowPreference( title = stringResource(R.string.pref_title_quick_device_sort), summary = stringResource(R.string.pref_summary_quick_device_sort), - onClick = onOpenReorderDevices, + onClick = { + haptic.contextClick() + onOpenReorderDevices() + }, ) ArrowPreference( title = stringResource(R.string.pref_title_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( title = stringResource(R.string.pref_title_password_autofill), summary = stringResource(R.string.pref_summary_password_autofill), onClick = { + haptic.contextClick() context.startActivity(LockscreenPasswordActivity.createIntent(context)) }, ) @@ -507,7 +484,7 @@ fun SettingsPage( checked = asBundle.realtimeClipboardSyncToDevice, onCheckedChange = { asBundle = asBundle.copy( - realtimeClipboardSyncToDevice = it + realtimeClipboardSyncToDevice = it, ) }, ) @@ -523,7 +500,7 @@ fun SettingsPage( checked = asBundle.fullscreenControlIgnoreSystemRotationLock, onCheckedChange = { asBundle = asBundle.copy( - fullscreenControlIgnoreSystemRotationLock = it + fullscreenControlIgnoreSystemRotationLock = it, ) }, ) @@ -533,7 +510,7 @@ fun SettingsPage( checked = asBundle.fullscreenControlBackToDevice, onCheckedChange = { asBundle = asBundle.copy( - fullscreenControlBackToDevice = it + fullscreenControlBackToDevice = it, ) }, ) @@ -543,7 +520,7 @@ fun SettingsPage( checked = asBundle.showFullscreenVirtualButtons, onCheckedChange = { asBundle = asBundle.copy( - showFullscreenVirtualButtons = it + showFullscreenVirtualButtons = it, ) }, ) @@ -564,7 +541,7 @@ fun SettingsPage( modeIndex = index, directionIndex = fullscreenVirtualButtonDock.directionIndex, ) - .toStoredValue() + .toStoredValue(), ) }, ) @@ -583,7 +560,7 @@ fun SettingsPage( modeIndex = fullscreenVirtualButtonDock.modeIndex, directionIndex = index, ) - .toStoredValue() + .toStoredValue(), ) }, ) @@ -593,7 +570,7 @@ fun SettingsPage( title = stringResource(R.string.pref_title_virtual_button_direction), summary = stringResource( if (fullscreenVirtualButtonDock.isFixed) R.string.dock_fixed - else R.string.dock_follow + else R.string.dock_follow, ) + stringResource(R.string.dock_display_on) + stringResource(fullscreenVirtualButtonDock.directionLabelResId), @@ -604,7 +581,7 @@ fun SettingsPage( onValueChange = { asBundle = asBundle.copy( fullscreenVirtualButtonHeightDp = - it.roundToInt().coerceIn(16, 80) + it.roundToInt().coerceIn(16, 80), ) }, valueRange = 16f..80f, @@ -618,7 +595,7 @@ fun SettingsPage( input.toIntOrNull()?.let { asBundle = asBundle.copy( fullscreenVirtualButtonHeightDp = - it.coerceIn(1, 160) + it.coerceIn(1, 160), ) } }, @@ -631,7 +608,7 @@ fun SettingsPage( checked = asBundle.showFullscreenFloatingButton, onCheckedChange = { asBundle = asBundle.copy( - showFullscreenFloatingButton = it + showFullscreenFloatingButton = it, ) }, ) @@ -643,7 +620,7 @@ fun SettingsPage( onValueChange = { asBundle = asBundle.copy( fullscreenFloatingButtonSizeDp = - it.roundToInt().coerceIn(32, 64) + it.roundToInt().coerceIn(32, 64), ) }, valueRange = 32f..64f, @@ -657,7 +634,7 @@ fun SettingsPage( input.toIntOrNull()?.let { asBundle = asBundle.copy( fullscreenFloatingButtonSizeDp = - it.coerceIn(16, 96) + it.coerceIn(16, 96), ) } }, @@ -668,7 +645,7 @@ fun SettingsPage( onValueChange = { asBundle = asBundle.copy( fullscreenFloatingButtonBackgroundAlphaPercent = - it.roundToInt().coerceIn(10, 100) + it.roundToInt().coerceIn(10, 100), ) }, valueRange = 10f..100f, @@ -682,7 +659,7 @@ fun SettingsPage( input.toIntOrNull()?.let { asBundle = asBundle.copy( fullscreenFloatingButtonBackgroundAlphaPercent = - it.coerceIn(10, 100) + it.coerceIn(10, 100), ) } }, @@ -693,7 +670,7 @@ fun SettingsPage( onValueChange = { asBundle = asBundle.copy( fullscreenFloatingButtonRingAlphaPercent = - it.roundToInt().coerceIn(0, 100) + it.roundToInt().coerceIn(0, 100), ) }, valueRange = 0f..100f, @@ -707,7 +684,7 @@ fun SettingsPage( input.toIntOrNull()?.let { asBundle = asBundle.copy( fullscreenFloatingButtonRingAlphaPercent = - it.coerceIn(0, 100) + it.coerceIn(0, 100), ) } }, @@ -720,7 +697,7 @@ fun SettingsPage( checked = asBundle.fullscreenCompatibilityMode, onCheckedChange = { asBundle = asBundle.copy( - fullscreenCompatibilityMode = it + fullscreenCompatibilityMode = it, ) }, ) @@ -752,11 +729,12 @@ fun SettingsPage( trailingIcon = { Row( modifier = Modifier - .padding(end = UiSpacing.Medium) + .padding(end = UiSpacing.Medium), ) { if (asBundle.customServerUri.isNotBlank()) IconButton( onClick = { + haptic.contextClick() asBundle = asBundle.copy( customServerUri = "", customServerVersion = "", @@ -768,7 +746,12 @@ fun SettingsPage( contentDescription = stringResource(R.string.cd_clear), ) } - IconButton(onClick = serverPicker.pick) { + IconButton( + onClick = { + haptic.contextClick() + serverPicker.pick() + }, + ) { Icon( imageVector = Icons.Rounded.FileOpen, contentDescription = stringResource(R.string.cd_select_file), @@ -795,7 +778,7 @@ fun SettingsPage( customServerVersionInput = "" asBundle = asBundle.copy( customServerVersion = customServerVersionInput - .ifBlank { AppSettings.CUSTOM_SERVER_VERSION.defaultValue } + .ifBlank { AppSettings.CUSTOM_SERVER_VERSION.defaultValue }, ) }, label = Scrcpy.DEFAULT_SERVER_VERSION, @@ -821,7 +804,7 @@ fun SettingsPage( serverRemotePathInput = "" asBundle = asBundle.copy( serverRemotePath = serverRemotePathInput - .ifBlank { AppSettings.SERVER_REMOTE_PATH.defaultValue } + .ifBlank { AppSettings.SERVER_REMOTE_PATH.defaultValue }, ) }, label = Scrcpy.DEFAULT_REMOTE_PATH, @@ -842,6 +825,7 @@ fun SettingsPage( title = stringResource(R.string.pref_title_battery_optimization), summary = stringResource(R.string.pref_summary_battery_optimization), onClick = { + haptic.contextClick() val appInfoArgs = Bundle().apply { putString("package", context.packageName) putInt("uid", context.applicationInfo.uid) @@ -866,10 +850,10 @@ fun SettingsPage( } val requestIntent = Intent( AndroidSettings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, - "package:${context.packageName}".toUri() + "package:${context.packageName}".toUri(), ) val fallbackIntent = Intent( - AndroidSettings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS + AndroidSettings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS, ) runCatching { context.startActivity(appDetailsIntent) } .recoverCatching { context.startActivity(requestIntent) } @@ -897,7 +881,7 @@ fun SettingsPage( adbKeyNameInput = "" asBundle = asBundle.copy( adbKeyName = adbKeyNameInput - .ifBlank { AppSettings.ADB_KEY_NAME.defaultValue } + .ifBlank { AppSettings.ADB_KEY_NAME.defaultValue }, ) }, label = AppSettings.ADB_KEY_NAME.defaultValue, @@ -931,7 +915,7 @@ fun SettingsPage( R.string.pref_adb_key_imported else -> R.string.pref_adb_key_generated - } + }, ), useLabelAsPlaceholder = true, modifier = Modifier.fillMaxWidth(), @@ -939,6 +923,7 @@ fun SettingsPage( Row(modifier = Modifier.padding(end = UiSpacing.Medium)) { IconButton( onClick = { + haptic.contextClick() scope.launch { runCatching { withContext(Dispatchers.IO) { @@ -972,7 +957,10 @@ fun SettingsPage( ) } IconButton( - onClick = { adbPrivateKeyPicker.launch(arrayOf("*/*")) }, + onClick = { + haptic.contextClick() + adbPrivateKeyPicker.launch(arrayOf("*/*")) + }, ) { Icon( imageVector = Icons.Rounded.FileOpen, @@ -997,7 +985,7 @@ fun SettingsPage( R.string.pref_adb_key_imported else -> R.string.pref_adb_key_generated - } + }, ) Text( text = stringResource(R.string.pref_title_adb_public_key), @@ -1013,7 +1001,10 @@ fun SettingsPage( trailingIcon = { Row(modifier = Modifier.padding(end = UiSpacing.Medium)) { IconButton( - onClick = { adbPublicKeyPicker.launch(arrayOf("*/*")) }, + onClick = { + haptic.contextClick() + adbPublicKeyPicker.launch(arrayOf("*/*")) + }, ) { Icon( imageVector = Icons.Rounded.FileOpen, @@ -1031,7 +1022,7 @@ fun SettingsPage( checked = asBundle.adbPairingAutoDiscoverOnDialogOpen, onCheckedChange = { asBundle = asBundle.copy( - adbPairingAutoDiscoverOnDialogOpen = it + adbPairingAutoDiscoverOnDialogOpen = it, ) }, ) @@ -1041,7 +1032,7 @@ fun SettingsPage( checked = asBundle.adbAutoReconnectPairedDevice, onCheckedChange = { asBundle = asBundle.copy( - adbAutoReconnectPairedDevice = it + adbAutoReconnectPairedDevice = it, ) }, ) @@ -1051,10 +1042,10 @@ fun SettingsPage( checked = asBundle.adbAutoLoadAppListOnConnect, onCheckedChange = { asBundle = asBundle.copy( - adbAutoLoadAppListOnConnect = it + adbAutoLoadAppListOnConnect = it, ) 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, onValueChange = { asBundle = asBundle.copy( - terminalFontSizeSp = it.roundToInt().toFloat() + terminalFontSizeSp = it.roundToInt().toFloat(), ) }, valueRange = 1f..32f, @@ -1083,7 +1074,7 @@ fun SettingsPage( onInputConfirm = { input -> input.toIntOrNull()?.let { 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()) { IconButton( onClick = { + haptic.contextClick() scope.launch { val cleared = clearTerminalFont(context) asBundle = asBundle.copy( - terminalFontDisplayName = "" + terminalFontDisplayName = "", ) AppRuntime.snackbar( 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( imageVector = Icons.Rounded.FileOpen, contentDescription = stringResource(R.string.cd_select_font), @@ -1153,7 +1150,7 @@ fun SettingsPage( checked = asBundle.clearLogsOnExit, onCheckedChange = { asBundle = asBundle.copy( - clearLogsOnExit = it + clearLogsOnExit = it, ) }, ) @@ -1163,7 +1160,7 @@ fun SettingsPage( checked = asBundle.hideDeviceLogs, onCheckedChange = { asBundle = asBundle.copy( - hideDeviceLogs = it + hideDeviceLogs = it, ) }, ) @@ -1176,7 +1173,10 @@ fun SettingsPage( ArrowPreference( title = stringResource(R.string.about_title), summary = updateSummary, - onClick = { navigator.push(RootScreen.About) }, + onClick = { + haptic.contextClick() + navigator.push(RootScreen.About) + }, ) } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/ArrowSlider.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/ArrowSlider.kt index 643dc0a..80a8e17 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/ArrowSlider.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/ArrowSlider.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import io.github.miuzarte.scrcpyforandroid.R +import io.github.miuzarte.scrcpyforandroid.ui.confirm import io.github.miuzarte.scrcpyforandroid.ui.contextClick import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Slider @@ -174,7 +175,7 @@ private fun SliderInputDialog( TextButton( text = stringResource(R.string.button_confirm), onClick = { - haptic.contextClick() + haptic.confirm() val inputValue = text.toFloatOrNull() ?: 0f if (inputValue >= inputValueRange.start && inputValue <= inputValueRange.endInclusive) { onConfirm(text.trim()) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt index cc4cdc4..c71d341 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt @@ -10,44 +10,16 @@ import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement -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.layout.* import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.AddLink -import androidx.compose.material.icons.rounded.CheckCircleOutline -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.material.icons.rounded.* +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.Storage import io.github.miuzarte.scrcpyforandroid.storage.Storage.scrcpyOptions +import io.github.miuzarte.scrcpyforandroid.ui.confirm import io.github.miuzarte.scrcpyforandroid.ui.contextClick import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -98,21 +71,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import top.yukonga.miuix.kmp.basic.Button -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.basic.* import top.yukonga.miuix.kmp.overlay.OverlayDialog -import top.yukonga.miuix.kmp.preference.ArrowPreference -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.preference.* import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.theme.MiuixTheme.isDynamicColor import top.yukonga.miuix.kmp.theme.MiuixTheme.textStyles @@ -230,6 +191,8 @@ internal fun PairingCard( onDiscoverTarget: (suspend () -> Pair?)? = null, onPair: (host: String, port: String, code: String) -> Unit, ) { + val haptic = LocalHapticFeedback.current + val showPairDialog = remember { mutableStateOf(false) } val holdDownState = remember { mutableStateOf(false) } @@ -237,6 +200,7 @@ internal fun PairingCard( ArrowPreference( title = stringResource(R.string.device_pairing_title), onClick = { + haptic.contextClick() showPairDialog.value = true holdDownState.value = true }, @@ -295,7 +259,7 @@ internal fun PreviewCard( var nextPointerLabel by rememberSaveable { mutableIntStateOf(1) } val alpha by animateFloatAsState( if (previewControlsVisible) 1f else 0f, - label = "preview-controls" + label = "preview-controls", ) val lifecycleOwner = LocalLifecycleOwner.current val bringIntoViewRequester = remember { BringIntoViewRequester() } @@ -361,7 +325,7 @@ internal fun PreviewCard( Card( modifier = Modifier .bringIntoViewRequester(bringIntoViewRequester) - .then(modifier) + .then(modifier), ) { Box( modifier = Modifier @@ -373,17 +337,20 @@ internal fun PreviewCard( when (event.actionMasked) { MotionEvent.ACTION_DOWN -> onTouchActiveChanged(true) MotionEvent.ACTION_UP, - MotionEvent.ACTION_CANCEL -> onTouchActiveChanged(false) + MotionEvent.ACTION_CANCEL, + -> onTouchActiveChanged(false) } touchEventHandler.handleMotionEvent(event) } } else { Modifier.pointerInput(sessionInfo) { - detectTapGestures(onTap = { - previewControlsVisible = !previewControlsVisible - }) + detectTapGestures( + onTap = { + previewControlsVisible = !previewControlsVisible + }, + ) } - } + }, ) .onSizeChanged { touchAreaSize = it }, ) { @@ -496,7 +463,6 @@ internal fun ConfigPanel( recentTasksEndActionText: String, onOpenRecentTasks: () -> Unit, onOpenAdvanced: () -> Unit, - onStartStopHaptic: () -> Unit, onStart: () -> Unit, onStop: () -> Unit, sessionInfo: Scrcpy.Session.SessionInfo?, @@ -505,6 +471,7 @@ internal fun ConfigPanel( onOpenFullscreen: () -> Unit = {}, reverseSideActions: Boolean = false, ) { + val haptic = LocalHapticFeedback.current val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) } val sessionStarted = sessionInfo != null @@ -563,7 +530,7 @@ internal fun ConfigPanel( checked = soBundle.audio, onCheckedChange = { soBundle = soBundle.copy( - audio = it + audio = it, ) }, enabled = !sessionStarted @@ -578,7 +545,7 @@ internal fun ConfigPanel( onSelectedIndexChange = { val codec = Codec.AUDIO[it] soBundle = soBundle.copy( - audioCodec = codec.string + audioCodec = codec.string, ) if (codec == Codec.FLAC) AppRuntime.snackbar(R.string.device_config_audio_codec_note) @@ -596,7 +563,7 @@ internal fun ConfigPanel( val idx = value.roundToInt() .coerceIn(0, ScrcpyPresets.AudioBitRate.lastIndex) soBundle = soBundle.copy( - audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000 + audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000, ) }, valueRange = 0f..ScrcpyPresets.AudioBitRate.lastIndex.toFloat(), @@ -626,7 +593,7 @@ internal fun ConfigPanel( onSelectedIndexChange = { val codec = Codec.VIDEO[it] soBundle = soBundle.copy( - videoCodec = codec.string + videoCodec = codec.string, ) if (codec == Codec.AV1) AppRuntime.snackbar(R.string.device_config_video_codec_note) @@ -639,7 +606,7 @@ internal fun ConfigPanel( value = soBundle.videoBitRate / 1_000_000f, onValueChange = { mbps -> soBundle = soBundle.copy( - videoBitRate = (mbps * 10).roundToInt() * (1_000_000 / 10) + videoBitRate = (mbps * 10).roundToInt() * (1_000_000 / 10), ) }, valueRange = 0f..40f, @@ -669,7 +636,7 @@ internal fun ConfigPanel( raw.toFloatOrNull()?.let { parsed -> if (parsed >= 0f) { soBundle = soBundle.copy( - videoBitRate = (parsed * 1_000_000f).roundToInt() + videoBitRate = (parsed * 1_000_000f).roundToInt(), ) } } @@ -681,7 +648,7 @@ internal fun ConfigPanel( ArrowPreference( title = stringResource( 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), endActions = { @@ -691,7 +658,10 @@ internal fun ConfigPanel( fontSize = textStyles.body2.fontSize, ) }, - onClick = onOpenAdvanced, + onClick = { + haptic.contextClick() + onOpenAdvanced() + }, enabled = !sessionStarted, ) @@ -704,7 +674,10 @@ internal fun ConfigPanel( fontSize = textStyles.body2.fontSize, ) }, - onClick = onOpenAllApps, + onClick = { + haptic.contextClick() + onOpenAllApps() + }, enabled = !busy && !adbConnecting, ) ArrowPreference( @@ -716,7 +689,10 @@ internal fun ConfigPanel( fontSize = textStyles.body2.fontSize, ) }, - onClick = onOpenRecentTasks, + onClick = { + haptic.contextClick() + onOpenRecentTasks() + }, enabled = !busy && !adbConnecting, ) @@ -738,7 +714,7 @@ internal fun ConfigPanel( if (isQuickConnected) TextButton( text = stringResource(R.string.button_disconnect), onClick = { - onStartStopHaptic() + haptic.contextClick() onDisconnect() }, modifier = Modifier.weight(sideButtonWeight), @@ -751,7 +727,7 @@ internal fun ConfigPanel( if (!sessionStarted) TextButton( text = stringResource(R.string.button_start), onClick = { - onStartStopHaptic() + haptic.contextClick() onStart() }, modifier = Modifier.weight(mainButtonWeight), @@ -761,7 +737,7 @@ internal fun ConfigPanel( if (sessionStarted) TextButton( text = stringResource(R.string.button_stop), onClick = { - onStartStopHaptic() + haptic.contextClick() onStop() }, modifier = Modifier.weight(mainButtonWeight), @@ -774,7 +750,7 @@ internal fun ConfigPanel( if (showFullscreenAction) TextButton( text = stringResource(R.string.button_fullscreen), onClick = { - onStartStopHaptic() + haptic.contextClick() onOpenFullscreen() }, modifier = Modifier.weight(sideButtonWeight), @@ -818,6 +794,8 @@ private fun PairingDialog( onDismissFinished: () -> Unit, onConfirm: (host: String, port: String, code: String) -> Unit, ) { + val haptic = LocalHapticFeedback.current + var host by rememberSaveable(showDialog) { mutableStateOf("") } var port by rememberSaveable(showDialog) { mutableStateOf("") } var code by rememberSaveable(showDialog) { mutableStateOf("") } @@ -869,7 +847,7 @@ private fun PairingDialog( keyboardActions = KeyboardActions( onNext = { focusManager.moveFocus(FocusDirection.Next) }, ), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) TextField( value = port, @@ -883,7 +861,7 @@ private fun PairingDialog( keyboardActions = KeyboardActions( onNext = { focusManager.moveFocus(FocusDirection.Next) }, ), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) TextField( value = code, @@ -897,7 +875,7 @@ private fun PairingDialog( keyboardActions = KeyboardActions( onDone = { focusManager.clearFocus() }, ), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } @@ -909,9 +887,10 @@ private fun PairingDialog( TextButton( text = stringResource( if (!discoveringPort) R.string.button_auto_discover - else R.string.button_discovering + else R.string.button_discovering, ), onClick = { + haptic.contextClick() if (enabled && onDiscoverTarget != null && !discoveringPort) scope.launch { doDiscover() } }, @@ -924,6 +903,7 @@ private fun PairingDialog( TextButton( text = stringResource(R.string.button_cancel), onClick = { + haptic.contextClick() onDismissRequest() }, modifier = Modifier.weight(1f), @@ -931,6 +911,7 @@ private fun PairingDialog( TextButton( text = stringResource(R.string.button_pair), onClick = { + haptic.confirm() onConfirm(host.trim(), port.trim(), code.trim()) onDismissRequest() }, @@ -1053,7 +1034,7 @@ fun ScrcpyVideoSurface( factory = { context -> ScrcpyInputSurfaceView(context).apply { currentSurfaceView = this - inputCallbacks = object : ScrcpyInputSurfaceView.InputCallbacks { + inputCallbacks = object: ScrcpyInputSurfaceView.InputCallbacks { override fun handleKeyEvent(event: KeyEvent): Boolean { val handler = onImeKeyEvent ?: return false taskScope.launch { @@ -1076,7 +1057,7 @@ fun ScrcpyVideoSurface( override fun handleDeleteSurroundingText( beforeLength: Int, - afterLength: Int + afterLength: Int, ): Boolean { val handler = onImeDeleteSurroundingText ?: return false taskScope.launch { @@ -1087,46 +1068,48 @@ fun ScrcpyVideoSurface( return true } } - holder.addCallback(object : SurfaceHolder.Callback { - override fun surfaceCreated(holder: SurfaceHolder) { - val newSurface = holder.surface - if (!newSurface.isValid) return - currentSurface = newSurface - // Register immediately when surface becomes available - if (latestSession != null) { - scope.launch { - NativeCoreFacade.attachVideoSurface(newSurface) + holder.addCallback( + object: SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { + val newSurface = holder.surface + if (!newSurface.isValid) return + currentSurface = newSurface + // Register immediately when surface becomes available + if (latestSession != null) { + scope.launch { + NativeCoreFacade.attachVideoSurface(newSurface) + } } } - } - override fun surfaceChanged( - holder: SurfaceHolder, - format: Int, - width: Int, - height: Int, - ) { - if (width <= 0 || height <= 0) return - if (!holder.surface.isValid) return - val surface = holder.surface - currentSurface = surface - if (latestSession != null) { - scope.launch { - NativeCoreFacade.attachVideoSurface(surface) + override fun surfaceChanged( + holder: SurfaceHolder, + format: Int, + width: Int, + height: Int, + ) { + if (width <= 0 || height <= 0) return + if (!holder.surface.isValid) return + val surface = holder.surface + currentSurface = surface + if (latestSession != null) { + scope.launch { + NativeCoreFacade.attachVideoSurface(surface) + } } } - } - override fun surfaceDestroyed(holder: SurfaceHolder) { - val surface = currentSurface - if (surface != null) { - taskScope.launch { - NativeCoreFacade.detachVideoSurface(surface) + override fun surfaceDestroyed(holder: SurfaceHolder) { + val surface = currentSurface + if (surface != null) { + taskScope.launch { + NativeCoreFacade.detachVideoSurface(surface) + } + currentSurface = null } - currentSurface = null } - } - }) + }, + ) } }, update = {}, @@ -1201,7 +1184,6 @@ internal fun DeviceTile( else colorScheme.surfaceContainer.copy(alpha = 0.6f), ), pressFeedbackType = if (!editing) PressFeedbackType.Sink else PressFeedbackType.None, - onClick = haptic::contextClick, ) { Row( modifier = Modifier @@ -1209,10 +1191,13 @@ internal fun DeviceTile( .then( if (!isConnected) Modifier.combinedClickable( - onClick = onClick, + onClick = { + haptic.contextClick() + onClick() + }, onLongClick = onLongClick, ) - else Modifier + else Modifier, ) .padding(UiSpacing.PageItem), verticalAlignment = Alignment.CenterVertically, @@ -1261,7 +1246,7 @@ internal fun DeviceTile( TextButton( text = stringResource( if (!isConnected) R.string.button_connect - else R.string.button_disconnect + else R.string.button_disconnect, ), onClick = onAction, enabled = actionEnabled && !actionInProgress, @@ -1303,7 +1288,7 @@ internal fun DeviceTile( onValueChange = { draftPortText = it.filter(Char::isDigit) draft = currentDraft.copy( - port = draftPortText?.toIntOrNull() ?: Defaults.ADB_PORT + port = draftPortText?.toIntOrNull() ?: Defaults.ADB_PORT, ) }, label = stringResource(R.string.label_port), @@ -1473,13 +1458,19 @@ internal fun QuickConnectCard( ) { TextButton( text = stringResource(R.string.button_add_device), - onClick = onAddDevice, + onClick = { + haptic.contextClick() + onAddDevice() + }, modifier = Modifier.weight(1f), enabled = enabled, ) TextButton( text = stringResource(R.string.button_direct_connect), - onClick = onConnect, + onClick = { + haptic.confirm() + onConnect() + }, modifier = Modifier.weight(1f), enabled = enabled, colors = ButtonDefaults.textButtonColorsPrimary(), diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/RecordPreferences.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/RecordPreferences.kt index 8ba35c8..f0c08f2 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/RecordPreferences.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/RecordPreferences.kt @@ -2,6 +2,7 @@ package io.github.miuzarte.scrcpyforandroid.widgets import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow 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.scrcpy.ClientOptions.RecordFormat 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.preference.ArrowPreference import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference @@ -23,6 +25,8 @@ fun RecordPreferences( enabled: Boolean, onRecordFormatChange: (String) -> Unit, ) { + val haptic = LocalHapticFeedback.current + val navigator = LocalRootNavigator.current val supportedFormats = remember { NativeRecordingSupport.supportedFormats } @@ -43,6 +47,7 @@ fun RecordPreferences( summary = "--record", enabled = enabled, onClick = { + haptic.contextClick() navigator.push(RootScreen.ScrcpyOptionRecord(profileId)) }, endActions = {