improve: complete haptic feedback

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

View File

@@ -6,14 +6,7 @@ import android.os.Bundle
import android.view.WindowManager
import 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),

View File

@@ -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,

View File

@@ -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)

View File

@@ -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()

View File

@@ -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(),
)

View File

@@ -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)
},
)
}
}

View File

@@ -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())

View File

@@ -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<String, Int>?)? = 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(),

View File

@@ -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 = {