diff --git a/README.md b/README.md index 0c56b31..029ba02 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,10 @@ Screenshot 2 Screenshot 3 Screenshot 4 + Screenshot 5

- Screenshot 5 + Screenshot 6

## Features @@ -24,10 +25,11 @@ ## 已知问题 / TODO -- 组件排序动画一坨 - 退出全屏时的横竖屏状态可能不对,断开 scrcpy 重连就好 - 如果受控机在锁屏时处理网络连接较慢,会导致应用界面无响应过长时间被系统杀掉(点名你米) -- 虚拟按键的截图实现方式为发送 `keycode 120`,安卓官方的定义为 `System Request / Print Screen key.`,不同的厂商有不同的实现,在某些类原生(`AxionOS`) 上的行为是软重启 +- 虚拟按键的截图实现方式为发送 +`keycode 120`,安卓官方([keycodes.h#349](https://android.googlesource.com/platform/frameworks/native/+/master/include/android/keycodes.h#349))的定义为 +`System Request / Print Screen key.`,不同的厂商有不同的实现,在某些类原生(`AxionOS`) 上的行为是软重启 - I18N ## 建议搭配模块 diff --git a/ScrcpyForAndroid.code-workspace b/ScrcpyForAndroid.code-workspace index 67c0246..5ed02f7 100644 --- a/ScrcpyForAndroid.code-workspace +++ b/ScrcpyForAndroid.code-workspace @@ -12,6 +12,10 @@ "name": "miuix", "path": "../miuix" }, + { + "name": "Reorderable", + "path": "../Reorderable" + }, { "name": "shizuku", "path": "../shizuku" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d7c4cac..2990d84 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -96,6 +96,7 @@ dependencies { implementation("org.lsposed.libcxx:libcxx:27.0.12077973") implementation("org.bouncycastle:bcpkix-jdk18on:1.80") implementation("org.conscrypt:conscrypt-android:2.5.2") + implementation("sh.calvin.reorderable:reorderable:3.0.0") testImplementation(libs.junit) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppDefaults.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppDefaults.kt index 9560cb6..a6c1103 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppDefaults.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppDefaults.kt @@ -65,9 +65,9 @@ object AppDefaults { const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = true const val KEEP_SCREEN_ON_WHEN_STREAMING = false const val DEVICE_PREVIEW_CARD_HEIGHT_DP = 320 - const val VIRTUAL_BUTTONS_OUTSIDE = "more,home,back" - const val VIRTUAL_BUTTONS_IN_MORE = - "app_switch,menu,notification,volume_up,volume_down,volume_mute,power,screenshot" + const val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = true + const val VIRTUAL_BUTTONS_LAYOUT = + "more:1,app_switch:1,home:0,back:1,menu:0,notification:0,volume_up:0,volume_down:0,volume_mute:0,power:0,screenshot:0" const val CUSTOM_SERVER_URI = "" diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt index 944d3e7..5272f06 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/AppPreferenceKeys.kt @@ -67,8 +67,8 @@ object AppPreferenceKeys { const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = "show_fullscreen_virtual_buttons" const val KEEP_SCREEN_ON_WHEN_STREAMING = "keep_screen_on_when_streaming" const val DEVICE_PREVIEW_CARD_HEIGHT_DP = "device_preview_card_height_dp" - const val VIRTUAL_BUTTONS_OUTSIDE = "virtual_buttons_outside" - const val VIRTUAL_BUTTONS_IN_MORE = "virtual_buttons_in_more" + const val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = "preview_virtual_button_show_text" + const val VIRTUAL_BUTTONS_LAYOUT = "virtual_buttons_layout" const val CUSTOM_SERVER_URI = "custom_server_uri" diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/haptics/AppHaptics.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/haptics/AppHaptics.kt index dc28085..e036e46 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/haptics/AppHaptics.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/haptics/AppHaptics.kt @@ -7,26 +7,87 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback +@Composable +fun performHaptics(type: HapticFeedbackType): Unit { + LocalHapticFeedback.current.performHapticFeedback(type) +} + @Immutable data class AppHaptics( - val press: () -> Unit, val confirm: () -> Unit, + val contextClick: () -> Unit, + val gestureEnd: () -> Unit, + val gestureThresholdActivate: () -> Unit, + val keyboardTap: () -> Unit, + val longPress: () -> Unit, + val reject: () -> Unit, + val segmentFrequentTick: () -> Unit, + val segmentTick: () -> Unit, + val textHandleMove: () -> Unit, + val toggleOff: () -> Unit, + val toggleOn: () -> Unit, + val virtualKey: () -> Unit, ) @Composable fun rememberAppHaptics(): AppHaptics { val hapticFeedback = LocalHapticFeedback.current - val pressAction = rememberUpdatedState { + + val performConfirm = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.Confirm) + } + val performContextClick = rememberUpdatedState { hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick) } - val confirmAction = rememberUpdatedState { - hapticFeedback.performHapticFeedback(HapticFeedbackType.Confirm) + val performGestureEnd = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd) + } + val performGestureThresholdActivate = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate) + } + val performKeyboardTan = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.KeyboardTap) + } + val performLongPress = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + } + val performReject = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.Reject) + } + val performSegmentFrequentTick = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) + } + val performSegmentTick = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentTick) + } + val performTextHandleMove = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove) + } + val performToggleOff = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.ToggleOff) + } + val performToggleOn = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.ToggleOn) + } + val performVirtualKey = rememberUpdatedState { + hapticFeedback.performHapticFeedback(HapticFeedbackType.VirtualKey) } return remember { AppHaptics( - press = { pressAction.value.invoke() }, - confirm = { confirmAction.value.invoke() }, + confirm = { performConfirm.value.invoke() }, + contextClick = { performContextClick.value.invoke() }, + gestureEnd = { performGestureEnd.value.invoke() }, + gestureThresholdActivate = { performGestureThresholdActivate.value.invoke() }, + keyboardTap = { performKeyboardTan.value.invoke() }, + longPress = { performLongPress.value.invoke() }, + reject = { performReject.value.invoke() }, + segmentFrequentTick = { performSegmentFrequentTick.value.invoke() }, + segmentTick = { performSegmentTick.value.invoke() }, + textHandleMove = { performTextHandleMove.value.invoke() }, + toggleOff = { performToggleOff.value.invoke() }, + toggleOn = { performToggleOn.value.invoke() }, + virtualKey = { performVirtualKey.value.invoke() }, ) } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt index 6bcf2ea..b343bb6 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DevicePage.kt @@ -5,7 +5,6 @@ import android.util.Log import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -48,11 +47,8 @@ import io.github.miuzarte.scrcpyforandroid.widgets.LogsPanel 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.ReorderableList import io.github.miuzarte.scrcpyforandroid.widgets.SectionSmallTitle -import io.github.miuzarte.scrcpyforandroid.widgets.SortDropPayload -import io.github.miuzarte.scrcpyforandroid.widgets.SortTransferDirection -import io.github.miuzarte.scrcpyforandroid.widgets.SortableCardItem -import io.github.miuzarte.scrcpyforandroid.widgets.SortableCardList import io.github.miuzarte.scrcpyforandroid.widgets.StatusCard import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions @@ -73,7 +69,6 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.concurrent.Executors -import kotlin.math.roundToInt private const val ADB_CONNECT_TIMEOUT_MS = 3_000L private const val ADB_KEEPALIVE_INTERVAL_MS = 3_000L @@ -125,9 +120,10 @@ fun DeviceTabScreen( nativeCore: NativeCoreFacade, snack: SnackbarHostState, scrollBehavior: ScrollBehavior, - virtualButtonsOutside: List, - virtualButtonsInMore: List, + virtualButtonsLayout: String, + showPreviewVirtualButtonText: Boolean, previewCardHeightDp: Int, + themeBaseIndex: Int, customServerUri: String?, serverRemotePath: String, onServerRemotePathChange: (String) -> Unit, @@ -218,8 +214,8 @@ fun DeviceTabScreen( ) { val context = LocalContext.current val haptics = rememberAppHaptics() - val virtualButtonLayout = remember(virtualButtonsOutside, virtualButtonsInMore) { - VirtualButtonActions.resolveLayout(virtualButtonsOutside, virtualButtonsInMore) + val virtualButtonLayout = remember(virtualButtonsLayout) { + VirtualButtonActions.splitLayout(VirtualButtonActions.parseStoredLayout(virtualButtonsLayout)) } val initialSettings = remember(context) { loadDevicePageSettings(context) } val scope = rememberCoroutineScope() @@ -945,34 +941,33 @@ fun DeviceTabScreen( SuperBottomSheet( show = showReorderSheet, - title = "调整设备排序", + title = "快速设备排序", onDismissRequest = { showReorderSheet = false }, ) { - SortableCardList( - title = "设备列表", - items = quickDevices.map { device -> - SortableCardItem( - id = device.id, - title = device.name.ifBlank { device.host }, - subtitle = "${device.host}:${device.port}", - ) - }, - modifier = Modifier.padding(horizontal = UiSpacing.CardContent), - transferDirection = SortTransferDirection.NONE, - onLongPressHaptic = { haptics.press() }, - onDrop = { payload: SortDropPayload -> - val fromIndex = quickDevices.indexOfFirst { it.id == payload.itemId } - if (fromIndex < 0) return@SortableCardList - val steps = (payload.deltaY / 54f).roundToInt() - if (steps == 0) return@SortableCardList - val toIndex = (fromIndex + steps).coerceIn(0, quickDevices.lastIndex) - if (toIndex == fromIndex) return@SortableCardList - val moved = quickDevices.removeAt(fromIndex) - quickDevices.add(toIndex, moved) - saveQuickDevices(context, quickDevices) - }, - ) - Spacer(Modifier.height(UiSpacing.Large)) + val list = remember { + ReorderableList( + { + quickDevices.map { device -> + ReorderableList.Item( + id = device.id, + title = device.name.ifBlank { device.host }, + subtitle = "${device.host}:${device.port}", + ) + } + }, + onSettle = { fromIndex, toIndex -> + if (fromIndex < 0) return@ReorderableList + val to = toIndex.coerceIn(0, quickDevices.size) + if (fromIndex == to) return@ReorderableList + + val moved = quickDevices.removeAt(fromIndex) + quickDevices.add(to.coerceIn(0, quickDevices.size), moved) + saveQuickDevices(context, quickDevices) + }, + ) + } + list() + Spacer(Modifier.height(UiSpacing.BottomSheetBottom)) } fun sendVirtualButtonAction(action: VirtualButtonAction) { @@ -1022,6 +1017,7 @@ fun DeviceTabScreen( sessionInfo = sessionInfo, busyLabel = null, connectedDeviceLabel = connectedDeviceLabel, + themeBaseIndex = themeBaseIndex, ) } @@ -1043,7 +1039,7 @@ fun DeviceTabScreen( } }, onAction = { - haptics.press() + haptics.contextClick() if (isConnectedTarget) { activeDeviceActionId = device.id runAdbConnect("断开 ADB", onFinished = { activeDeviceActionId = null }) { @@ -1114,7 +1110,7 @@ fun DeviceTabScreen( } }, ) - SectionSmallTitle(text = "无线配对") + SectionSmallTitle("无线配对") // "使用配对码配对设备" PairingCard( busy = busy, @@ -1167,7 +1163,7 @@ fun DeviceTabScreen( audioCodec = audioCodec, onAudioCodecChange = onAudioCodecChange, onOpenAdvanced = onOpenAdvancedPage, - onStartStopHaptic = { haptics.press() }, + onStartStopHaptic = { haptics.contextClick() }, onStart = { runBusy("启动 scrcpy") { if (noVideo && !audioEnabled) { @@ -1316,17 +1312,13 @@ fun DeviceTabScreen( previewHeightDp = previewCardHeightDp.coerceAtLeast(120), controlsVisible = previewControlsVisible, onTapped = { - previewControlsVisible = true - scope.launch { - delay(2000) - previewControlsVisible = false - } + previewControlsVisible = !previewControlsVisible }, onOpenFullscreen = { val info = sessionInfo ?: return@PreviewCard onOpenFullscreenPage(info) }, - onOpenFullscreenHaptic = { haptics.press() }, + onOpenFullscreenHaptic = { haptics.contextClick() }, ) } item { @@ -1334,8 +1326,7 @@ fun DeviceTabScreen( busy = busy, outsideActions = virtualButtonLayout.first, moreActions = virtualButtonLayout.second, - onPressHaptic = { haptics.press() }, - onConfirmHaptic = { haptics.confirm() }, + showText = showPreviewVirtualButtonText, onAction = ::sendVirtualButtonAction, ) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt index 5d12f2c..b0b301b 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlPage.kt @@ -38,8 +38,7 @@ data class FullscreenControlLaunch( fun FullscreenControlPage( launch: FullscreenControlLaunch, nativeCore: NativeCoreFacade, - virtualButtonsOutside: List, - virtualButtonsInMore: List, + virtualButtonsLayout: String, showDebugInfo: Boolean, showVirtualButtons: Boolean, onVideoSizeChanged: (width: Int, height: Int) -> Unit, @@ -51,8 +50,8 @@ fun FullscreenControlPage( val context = LocalContext.current val haptics = rememberAppHaptics() val activity = remember(context) { context as? Activity } - val virtualButtonLayout = remember(virtualButtonsOutside, virtualButtonsInMore) { - VirtualButtonActions.resolveLayout(virtualButtonsOutside, virtualButtonsInMore) + val virtualButtonLayout = remember(virtualButtonsLayout) { + VirtualButtonActions.splitLayout(VirtualButtonActions.parseStoredLayout(virtualButtonsLayout)) } val bar = remember(virtualButtonLayout) { VirtualButtonBar( @@ -150,8 +149,6 @@ fun FullscreenControlPage( if (showVirtualButtons) { bar.Fullscreen( modifier = Modifier.align(Alignment.BottomCenter), - onPressHaptic = { haptics.press() }, - onConfirmHaptic = { haptics.confirm() }, onAction = { action -> action.keycode?.let(::sendKeycode) }, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt index a4410ff..3b3fd80 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/MainPage.kt @@ -19,9 +19,9 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Devices -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.rounded.Devices +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material.icons.rounded.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -54,7 +54,6 @@ import io.github.miuzarte.scrcpyforandroid.services.MainSettings import io.github.miuzarte.scrcpyforandroid.services.loadDevicePageSettings import io.github.miuzarte.scrcpyforandroid.services.loadMainSettings import io.github.miuzarte.scrcpyforandroid.services.saveMainSettings -import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -82,8 +81,8 @@ private enum class MainTabDestination( val label: String, val icon: ImageVector, ) { - Device(title = "设备", label = "设备", icon = Icons.Default.Devices), - Settings(title = "设置", label = "设置", icon = Icons.Default.Settings), + Device(title = "设备", label = "设备", icon = Icons.Rounded.Devices), + Settings(title = "设置", label = "设置", icon = Icons.Rounded.Settings), } private sealed interface RootScreen : NavKey { @@ -134,14 +133,10 @@ fun MainPage() { var monetEnabled by rememberSaveable { mutableStateOf(initialSettings.monetEnabled) } var fullscreenDebugInfoEnabled by rememberSaveable { mutableStateOf(initialSettings.fullscreenDebugInfoEnabled) } var showFullscreenVirtualButtons by rememberSaveable { mutableStateOf(initialSettings.showFullscreenVirtualButtons) } + var showPreviewVirtualButtonText by rememberSaveable { mutableStateOf(initialSettings.showPreviewVirtualButtonText) } var keepScreenOnWhenStreamingEnabled by rememberSaveable { mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled) } var devicePreviewCardHeightDp by rememberSaveable { mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) } - var virtualButtonsOutside by rememberSaveable(stateSaver = stringListSaver) { - mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsOutside)) - } - var virtualButtonsInMore by rememberSaveable(stateSaver = stringListSaver) { - mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsInMore)) - } + var virtualButtonsLayout by rememberSaveable { mutableStateOf(initialSettings.virtualButtonsLayout) } var customServerUri by rememberSaveable { mutableStateOf(initialSettings.customServerUri) } var serverRemotePath by rememberSaveable { mutableStateOf(initialSettings.serverRemotePath) } var adbKeyName by rememberSaveable { mutableStateOf(initialSettings.adbKeyName) } @@ -241,10 +236,10 @@ fun MainPage() { monetEnabled, fullscreenDebugInfoEnabled, showFullscreenVirtualButtons, + showPreviewVirtualButtonText, keepScreenOnWhenStreamingEnabled, devicePreviewCardHeightDp, - virtualButtonsOutside, - virtualButtonsInMore, + virtualButtonsLayout, customServerUri, serverRemotePath, adbKeyName, @@ -262,10 +257,10 @@ fun MainPage() { monetEnabled = monetEnabled, fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled, showFullscreenVirtualButtons = showFullscreenVirtualButtons, + showPreviewVirtualButtonText = showPreviewVirtualButtonText, keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled, devicePreviewCardHeightDp = devicePreviewCardHeightDp, - virtualButtonsOutside = VirtualButtonActions.encodeStoredIds(virtualButtonsOutside), - virtualButtonsInMore = VirtualButtonActions.encodeStoredIds(virtualButtonsInMore), + virtualButtonsLayout = virtualButtonsLayout, customServerUri = customServerUri, serverRemotePath = serverRemotePath, adbKeyName = adbKeyName, @@ -398,7 +393,7 @@ fun MainPage() { holdDownState = showDeviceMenu, ) { Icon( - Icons.Default.MoreVert, + Icons.Rounded.MoreVert, contentDescription = "更多" ) } @@ -429,8 +424,10 @@ fun MainPage() { nativeCore = nativeCore, snack = snackHostState, scrollBehavior = deviceScrollBehavior, - virtualButtonsOutside = virtualButtonsOutside, - virtualButtonsInMore = virtualButtonsInMore, + virtualButtonsLayout = virtualButtonsLayout, + showPreviewVirtualButtonText = showPreviewVirtualButtonText, + previewCardHeightDp = devicePreviewCardHeightDp, + themeBaseIndex = themeBaseIndex, customServerUri = customServerUri, serverRemotePath = serverRemotePath, onServerRemotePathChange = { serverRemotePath = it }, @@ -556,7 +553,6 @@ fun MainPage() { ), ) }, - previewCardHeightDp = devicePreviewCardHeightDp, adbPairingAutoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen, adbAutoReconnectPairedDevice = adbAutoReconnectPairedDevice, adbMdnsLanDiscoveryEnabled = adbMdnsLanDiscoveryEnabled, @@ -589,6 +585,12 @@ fun MainPage() { onDevicePreviewCardHeightDpChange = { devicePreviewCardHeightDp = it.coerceAtLeast(120) }, + onOpenReorderDevices = { + openReorderDevicesAction?.invoke() + }, + onOpenVirtualButtonOrder = { + rootBackStack.add(RootScreen.VirtualButtonOrder) + }, showFullscreenVirtualButtons = showFullscreenVirtualButtons, onShowFullscreenVirtualButtonsChange = { showFullscreenVirtualButtons = it @@ -756,12 +758,12 @@ fun MainPage() { VirtualButtonOrderPage( contentPadding = pagePadding, scrollBehavior = advancedScrollBehavior, - outsideIds = virtualButtonsOutside, - moreIds = virtualButtonsInMore, - onLayoutChange = { outside, more -> - virtualButtonsOutside = outside - virtualButtonsInMore = more + layoutString = virtualButtonsLayout, + onLayoutChange = { layout -> + virtualButtonsLayout = layout }, + showPreviewText = showPreviewVirtualButtonText, + onShowPreviewTextChange = { showPreviewVirtualButtonText = it }, ) } } @@ -770,8 +772,7 @@ fun MainPage() { FullscreenControlPage( launch = screen.launch, nativeCore = nativeCore, - virtualButtonsOutside = virtualButtonsOutside, - virtualButtonsInMore = virtualButtonsInMore, + virtualButtonsLayout = virtualButtonsLayout, showDebugInfo = fullscreenDebugInfoEnabled, showVirtualButtons = showFullscreenVirtualButtons, onVideoSizeChanged = { width, height -> @@ -817,7 +818,7 @@ private fun DeviceMenuPopup( ) { ListPopupColumn { DeviceMenuPopupItem( - text = "调整设备排序", + text = "快速设备排序", optionSize = 3, index = 0, onClick = onReorderDevices, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt index a0a03a4..fdad761 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsPage.kt @@ -1,5 +1,6 @@ package io.github.miuzarte.scrcpyforandroid.pages +import android.content.Intent import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -7,11 +8,13 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Clear -import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material.icons.rounded.Clear +import androidx.compose.material.icons.rounded.FileOpen import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight +import androidx.core.net.toUri import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn @@ -23,6 +26,7 @@ import top.yukonga.miuix.kmp.basic.IconButton 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.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.extra.SuperSwitch import top.yukonga.miuix.kmp.theme.ColorSchemeMode @@ -66,6 +70,8 @@ fun SettingsScreen( devicePreviewCardHeightDp: Int, onDevicePreviewCardHeightDpChange: (Int) -> Unit, showFullscreenVirtualButtons: Boolean, + onOpenReorderDevices: () -> Unit, + onOpenVirtualButtonOrder: () -> Unit, onShowFullscreenVirtualButtonsChange: (Boolean) -> Unit, customServerUri: String?, onPickServer: () -> Unit, @@ -81,6 +87,7 @@ fun SettingsScreen( scrollBehavior: ScrollBehavior, ) { val baseModeItems = THEME_BASE_OPTIONS.map { it.label } + val context = LocalContext.current // 设置 AppPageLazyColumn( @@ -88,7 +95,7 @@ fun SettingsScreen( scrollBehavior = scrollBehavior, ) { item { - SectionSmallTitle(text = "主题") + SectionSmallTitle("主题") Card { SuperDropdown( title = "外观模式", @@ -105,7 +112,7 @@ fun SettingsScreen( ) } - SectionSmallTitle(text = "投屏") + SectionSmallTitle("投屏") Card { SuperSwitch( title = "启用调试信息", @@ -114,8 +121,8 @@ fun SettingsScreen( onCheckedChange = onFullscreenDebugInfoEnabledChange, ) SuperSwitch( - title = "投屏时不允许息屏", - summary = "Scrcpy 启动后保持本机常亮,避免锁屏导致 ADB 断开", + title = "投屏时保持屏幕常亮", + summary = "Scrcpy 启动后保持本机屏幕常亮,避免锁屏导致 ADB 断开", checked = keepScreenOnWhenStreamingEnabled, onCheckedChange = onKeepScreenOnWhenStreamingEnabledChange, ) @@ -140,6 +147,16 @@ fun SettingsScreen( ?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) } }, ) + SuperArrow( + title = "快速设备排序", + summary = "手动排序设备页的快速设备", + onClick = onOpenReorderDevices, + ) + SuperArrow( + title = "虚拟按钮排序", + summary = "手动排序预览/全屏时的虚拟按钮,并选择哪些按钮展示在外", + onClick = onOpenVirtualButtonOrder, + ) SuperSwitch( title = "全屏显示虚拟按钮", summary = "在全屏控制页底部显示返回键、主页键等虚拟按钮", @@ -148,7 +165,7 @@ fun SettingsScreen( ) } - SectionSmallTitle(text = "scrcpy-server") + SectionSmallTitle("scrcpy-server") Card { Spacer(modifier = Modifier.padding(top = UiSpacing.CardContent)) Text( @@ -171,10 +188,10 @@ fun SettingsScreen( trailingIcon = { Row(modifier = Modifier.padding(end = UiSpacing.SectionTitleLeadingGap)) { if (customServerUri != null) IconButton(onClick = onClearServer) { - Icon(Icons.Default.Clear, contentDescription = "清空") + Icon(Icons.Rounded.Clear, contentDescription = "清空") } IconButton(onClick = onPickServer) { - Icon(Icons.Default.FolderOpen, contentDescription = "选择文件") + Icon(Icons.Rounded.FileOpen, contentDescription = "选择文件") } } }, @@ -199,7 +216,7 @@ fun SettingsScreen( ) } - SectionSmallTitle(text = "ADB") + SectionSmallTitle("ADB") Card { Text( text = "自定义 ADB 密钥名", @@ -232,6 +249,21 @@ fun SettingsScreen( onCheckedChange = onAdbAutoReconnectPairedDeviceChange, ) } + + SectionSmallTitle("关于") + Card { + SuperArrow( + title = "前往仓库", + summary = "github.com/Miuzarte/ScrcpyForAndroid", + onClick = { + val intent = Intent( + Intent.ACTION_VIEW, + "https://github.com/Miuzarte/ScrcpyForAndroid".toUri() + ) + context.startActivity(intent) + }, + ) + } } // TODO: 放进 [AppPageLazyColumn] 里 diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt index b0bf6c7..9f4f3cf 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/VirtualButtonOrderPage.kt @@ -1,169 +1,86 @@ package io.github.miuzarte.scrcpyforandroid.pages 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.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.toMutableStateList -import androidx.compose.ui.Modifier -import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing -import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics +import androidx.compose.runtime.setValue import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn -import io.github.miuzarte.scrcpyforandroid.widgets.SortDropPayload -import io.github.miuzarte.scrcpyforandroid.widgets.SortTransferDirection -import io.github.miuzarte.scrcpyforandroid.widgets.SortableCardItem -import io.github.miuzarte.scrcpyforandroid.widgets.SortableCardList +import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.ScrollBehavior -import top.yukonga.miuix.kmp.basic.Text -import kotlin.math.roundToInt - -private const val LIST_REORDER_STEP_PX = 54f -private const val LIST_TRANSFER_STEP_PX = 72f +import top.yukonga.miuix.kmp.extra.SuperSwitch @Composable internal fun VirtualButtonOrderPage( contentPadding: PaddingValues, scrollBehavior: ScrollBehavior, - outsideIds: List, - moreIds: List, - onLayoutChange: (outside: List, more: List) -> Unit, + layoutString: String, + onLayoutChange: (String) -> Unit, + showPreviewText: Boolean, + onShowPreviewTextChange: (Boolean) -> Unit, ) { - val haptics = rememberAppHaptics() - val normalized = remember(outsideIds, moreIds) { - VirtualButtonActions.resolveLayout(outsideIds, moreIds) - } - val outsideState = remember { normalized.first.map { it.id }.toMutableStateList() } - val moreState = remember { normalized.second.map { it.id }.toMutableStateList() } - - LaunchedEffect(outsideIds, moreIds) { - val resolved = VirtualButtonActions.resolveLayout(outsideIds, moreIds) - outsideState.clear() - outsideState.addAll(resolved.first.map { it.id }) - moreState.clear() - moreState.addAll(resolved.second.map { it.id }) + var buttonItems by remember(layoutString) { + mutableStateOf(VirtualButtonActions.parseStoredLayout(layoutString)) } - fun emit() { - onLayoutChange(outsideState.toList(), moreState.toList()) - } - - fun reorderInside( - list: androidx.compose.runtime.snapshots.SnapshotStateList, - itemId: String, - deltaY: Float - ) { - val fromIndex = list.indexOf(itemId) - if (fromIndex < 0) return - - val steps = (deltaY / LIST_REORDER_STEP_PX).roundToInt() - if (steps == 0) return - - val toIndex = (fromIndex + steps).coerceIn(0, list.lastIndex) - if (toIndex == fromIndex) return - - val moved = list.removeAt(fromIndex) - list.add(toIndex, moved) - emit() - } - - fun transferToOther( - from: androidx.compose.runtime.snapshots.SnapshotStateList, - to: androidx.compose.runtime.snapshots.SnapshotStateList, - itemId: String, - deltaY: Float, - ) { - val fromIndex = from.indexOf(itemId) - if (fromIndex < 0) return - - val steps = (deltaY / LIST_REORDER_STEP_PX).roundToInt() - val baseIndex = fromIndex + steps - val insertIndex = baseIndex.coerceIn(0, to.size) - - from.removeAt(fromIndex) - to.add(insertIndex, itemId) - emit() - } - - fun handleOutsideDrop(payload: SortDropPayload) { - val transfer = - payload.transferDirection == SortTransferDirection.TO_RIGHT && payload.deltaX >= LIST_TRANSFER_STEP_PX - if (transfer) { - if (payload.itemId == VirtualButtonAction.MORE.id) return - transferToOther(outsideState, moreState, payload.itemId, payload.deltaY) - } else { - reorderInside(outsideState, payload.itemId, payload.deltaY) - } - } - - fun handleMoreDrop(payload: SortDropPayload) { - val transfer = - payload.transferDirection == SortTransferDirection.TO_LEFT && payload.deltaX <= -LIST_TRANSFER_STEP_PX - if (transfer) { - transferToOther(moreState, outsideState, payload.itemId, payload.deltaY) - } else { - reorderInside(moreState, payload.itemId, payload.deltaY) - } + fun emitChanges() { + onLayoutChange(VirtualButtonActions.encodeStoredLayout(buttonItems)) } AppPageLazyColumn( contentPadding = contentPadding, scrollBehavior = scrollBehavior, ) { + // 按钮显示文本开关 item { - Card(modifier = Modifier.fillMaxWidth()) { - Text( - text = "长按可在两列间拖动排序\n“更多”不可移入右侧菜单", - modifier = Modifier.padding(UiSpacing.CardContent), + Card { + SuperSwitch( + title = "按钮显示文本", + summary = "超过3个建议关闭,只对预览卡下方的虚拟按钮生效", + checked = showPreviewText, + onCheckedChange = onShowPreviewTextChange, ) } } item { - Row( - modifier = Modifier.fillMaxWidth(), - ) { - SortableCardList( - title = "外部按钮", - items = outsideState.map { id -> - val action = VirtualButtonAction.entries.first { it.id == id } - SortableCardItem( + ReorderableList( + itemsProvider = { + buttonItems.map { item -> + val action = item.action + ReorderableList.Item( id = action.id, - title = action.title, - subtitle = "显示在预览与全屏底部", + icon = action.icon, + title = if (action.keycode == null) action.title else "${action.title} (${action.keycode})", + subtitle = if (item.showOutside) "显示在外部" else "显示在更多菜单内", + checked = item.showOutside, + checkboxEnabled = action != VirtualButtonAction.MORE, ) - }, - modifier = Modifier - .weight(1f) - .padding(end = UiSpacing.Small), - transferDirection = SortTransferDirection.TO_RIGHT, - onLongPressHaptic = haptics.press, - onDrop = ::handleOutsideDrop, - ) - - SortableCardList( - title = "更多菜单", - items = moreState.map { id -> - val action = VirtualButtonAction.entries.first { it.id == id } - SortableCardItem( - id = action.id, - title = action.title, - subtitle = "显示在“更多”弹窗中", - ) - }, - modifier = Modifier - .weight(1f) - .padding(start = UiSpacing.Small), - transferDirection = SortTransferDirection.TO_LEFT, - onLongPressHaptic = haptics.press, - onDrop = ::handleMoreDrop, - ) - } + } + }, + orientation = ReorderableList.Orientation.Column, + onSettle = { fromIndex, toIndex -> + buttonItems = buttonItems.toMutableList().apply { + add(toIndex, removeAt(fromIndex)) + } + emitChanges() + }, + showCheckbox = true, + onCheckboxChange = { id, checked -> + buttonItems = buttonItems.map { item -> + if (item.action.id == id) { + item.copy(showOutside = checked) + } else { + item + } + } + emitChanges() + }, + )() } } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/PageLayouts.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/PageLayouts.kt index bd863f9..f884751 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/PageLayouts.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scaffolds/PageLayouts.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -22,6 +24,7 @@ fun AppPageLazyColumn( contentPadding: PaddingValues, scrollBehavior: ScrollBehavior, modifier: Modifier = Modifier, + state: LazyListState = rememberLazyListState(), itemSpacing: Dp = UiSpacing.PageItem, horizontalPadding: Dp = UiSpacing.PageHorizontal, verticalPadding: Dp = UiSpacing.PageVertical, @@ -44,6 +47,7 @@ fun AppPageLazyColumn( .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .padding(contentPadding), + state = state, contentPadding = PaddingValues(horizontal = horizontalPadding, vertical = verticalPadding), verticalArrangement = Arrangement.spacedBy(itemSpacing), content = content, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppSettingsStore.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppSettingsStore.kt index 7e5a93e..d9d2c1e 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppSettingsStore.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/AppSettingsStore.kt @@ -13,10 +13,10 @@ internal data class MainSettings( val monetEnabled: Boolean = AppDefaults.MONET, val fullscreenDebugInfoEnabled: Boolean = AppDefaults.FULLSCREEN_DEBUG_INFO, val showFullscreenVirtualButtons: Boolean = AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, + val showPreviewVirtualButtonText: Boolean = AppDefaults.PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT, val keepScreenOnWhenStreamingEnabled: Boolean = AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING, val devicePreviewCardHeightDp: Int = AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP, - val virtualButtonsOutside: String = AppDefaults.VIRTUAL_BUTTONS_OUTSIDE, - val virtualButtonsInMore: String = AppDefaults.VIRTUAL_BUTTONS_IN_MORE, + val virtualButtonsLayout: String = AppDefaults.VIRTUAL_BUTTONS_LAYOUT, val customServerUri: String? = AppDefaults.CUSTOM_SERVER_URI, val serverRemotePath: String = AppDefaults.SERVER_REMOTE_PATH_INPUT, val adbKeyName: String = AppDefaults.ADB_KEY_NAME_INPUT, @@ -101,6 +101,10 @@ internal fun loadMainSettings(context: Context): MainSettings { AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, ), + showPreviewVirtualButtonText = prefs.getBoolean( + AppPreferenceKeys.PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT, + AppDefaults.PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT, + ), keepScreenOnWhenStreamingEnabled = prefs.getBoolean( AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING, AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING, @@ -109,14 +113,10 @@ internal fun loadMainSettings(context: Context): MainSettings { AppPreferenceKeys.DEVICE_PREVIEW_CARD_HEIGHT_DP, AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP, ).coerceAtLeast(120), - virtualButtonsOutside = prefs.getString( - AppPreferenceKeys.VIRTUAL_BUTTONS_OUTSIDE, - AppDefaults.VIRTUAL_BUTTONS_OUTSIDE, - ).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_OUTSIDE }, - virtualButtonsInMore = prefs.getString( - AppPreferenceKeys.VIRTUAL_BUTTONS_IN_MORE, - AppDefaults.VIRTUAL_BUTTONS_IN_MORE, - ).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_IN_MORE }, + virtualButtonsLayout = prefs.getString( + AppPreferenceKeys.VIRTUAL_BUTTONS_LAYOUT, + AppDefaults.VIRTUAL_BUTTONS_LAYOUT, + ).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_LAYOUT }, customServerUri = prefs.getString( AppPreferenceKeys.CUSTOM_SERVER_URI, AppDefaults.CUSTOM_SERVER_URI @@ -177,6 +177,10 @@ internal fun saveMainSettings(context: Context, settings: MainSettings) { AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, settings.showFullscreenVirtualButtons, ) + .putBoolean( + AppPreferenceKeys.PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT, + settings.showPreviewVirtualButtonText, + ) .putBoolean( AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING, settings.keepScreenOnWhenStreamingEnabled, @@ -186,12 +190,8 @@ internal fun saveMainSettings(context: Context, settings: MainSettings) { settings.devicePreviewCardHeightDp.coerceAtLeast(120), ) .putString( - AppPreferenceKeys.VIRTUAL_BUTTONS_OUTSIDE, - settings.virtualButtonsOutside, - ) - .putString( - AppPreferenceKeys.VIRTUAL_BUTTONS_IN_MORE, - settings.virtualButtonsInMore, + AppPreferenceKeys.VIRTUAL_BUTTONS_LAYOUT, + settings.virtualButtonsLayout, ) .putString( AppPreferenceKeys.CUSTOM_SERVER_URI, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt index c9cfc26..0a28d54 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/DeviceWidgets.kt @@ -30,9 +30,9 @@ 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.filled.AddLink -import androidx.compose.material.icons.filled.Fullscreen +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 @@ -123,18 +123,28 @@ internal fun StatusCard( sessionInfo: ScrcpySessionInfo?, busyLabel: String?, connectedDeviceLabel: String, + themeBaseIndex: Int, ) { val cleanStatusLine = normalizeStatusLine(statusLine) + + // 根据应用主题设置决定是否使用深色模式 + val isDarkTheme = when (themeBaseIndex) { + 0 -> isSystemInDarkTheme() // 跟随系统 + 1 -> false // 浅色 + 2 -> true // 深色 + else -> isSystemInDarkTheme() + } + val spec = when { streaming && sessionInfo != null -> { val streamCardColor = when { isDynamicColor -> MiuixTheme.colorScheme.secondaryContainer - isSystemInDarkTheme() -> Color(0xFF1A3825) + isDarkTheme -> Color(0xFF1A3825) else -> Color(0xFFDFFAE4) } val streamTextColor = when { isDynamicColor -> MiuixTheme.colorScheme.onSecondaryContainer - isSystemInDarkTheme() -> Color.White + isDarkTheme -> Color.White else -> Color(0xFF111111) } val streamIconColor = if (isDynamicColor) { @@ -300,12 +310,17 @@ internal fun PreviewCard( ) { Button( onClick = { - onOpenFullscreenHaptic?.invoke() - onOpenFullscreen() + if (alpha > 0.1) { + onOpenFullscreenHaptic?.invoke() + onOpenFullscreen() + } }, modifier = Modifier.alpha(alpha), ) { - Icon(Icons.Default.Fullscreen, contentDescription = "全屏") + Icon( + Icons.Rounded.Fullscreen, + contentDescription = "全屏", + ) Spacer(Modifier.width(UiSpacing.SectionTitleBottom)) Text("全屏") } @@ -320,8 +335,7 @@ internal fun VirtualButtonCard( busy: Boolean, outsideActions: List, moreActions: List, - onPressHaptic: () -> Unit, - onConfirmHaptic: () -> Unit, + showText: Boolean, onAction: (VirtualButtonAction) -> Unit, ) { val bar = remember(outsideActions, moreActions) { @@ -334,8 +348,7 @@ internal fun VirtualButtonCard( Card { bar.Preview( enabled = !busy, - onPressHaptic = onPressHaptic, - onConfirmHaptic = onConfirmHaptic, + showText = showText, onAction = onAction, modifier = Modifier .fillMaxWidth() @@ -374,7 +387,7 @@ internal fun ConfigPanel( val audioBitRatePresetIndex = presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate) - SectionSmallTitle(text = "Scrcpy") + SectionSmallTitle("Scrcpy") Card { SuperSwitch( title = "音频转发", @@ -1021,7 +1034,7 @@ internal fun DeviceTile( else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f), ), pressFeedbackType = PressFeedbackType.Sink, - onClick = haptics.press, + onClick = haptics.contextClick, ) { Row( modifier = Modifier @@ -1109,7 +1122,7 @@ internal fun QuickConnectCard( verticalAlignment = Alignment.CenterVertically, ) { Icon( - Icons.Default.AddLink, + Icons.Rounded.AddLink, contentDescription = "快速连接", tint = MiuixTheme.colorScheme.onPrimaryContainer, ) @@ -1180,7 +1193,7 @@ internal fun DeviceEditorScreen( .padding(contentPadding) .padding(UiSpacing.PageHorizontal), ) { - SectionSmallTitle(text = "编辑设备") + SectionSmallTitle("编辑设备") Card { TextField( value = name, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/ReorderableList.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/ReorderableList.kt new file mode 100644 index 0000000..6e0c1b8 --- /dev/null +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/ReorderableList.kt @@ -0,0 +1,217 @@ +package io.github.miuzarte.scrcpyforandroid.widgets + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.DragIndicator +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing +import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics +import sh.calvin.reorderable.ReorderableColumn +import sh.calvin.reorderable.ReorderableRow +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Checkbox +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.theme.MiuixTheme + +class ReorderableList( + private val itemsProvider: () -> List, + private val orientation: Orientation = Orientation.Column, + private val onSettle: (fromIndex: Int, toIndex: Int) -> Unit = { _, _ -> }, + private val modifier: Modifier = Modifier, + private val showCheckbox: Boolean = false, + private val onCheckboxChange: ((String, Boolean) -> Unit)? = null, +) { + enum class Orientation { Column, Row } + + data class Item( + val id: String, + val icon: ImageVector? = null, + val title: String, + val subtitle: String, + val checked: Boolean = true, + val checkboxEnabled: Boolean = true, + ) + + @Composable + operator fun invoke() { + val haptics = rememberAppHaptics() + val items = itemsProvider() + when (orientation) { + Orientation.Column -> { + ReorderableColumn( + list = items, + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(UiSpacing.Small), + onSettle = onSettle, + onMove = haptics.segmentTick, + ) { _, item, _ -> + key(item.id) { + ReorderableItem { + Card( + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = UiSpacing.CardTitle, + vertical = UiSpacing.FieldLabelBottom + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + if (item.icon != null) { + Icon( + item.icon, + contentDescription = item.title + ) + Spacer(Modifier.padding(horizontal = 4.dp)) + } + Column { + Text( + text = item.title, + color = MiuixTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + ) + if (item.subtitle.isNotBlank()) { + Text( + text = item.subtitle, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + fontSize = 13.sp, + ) + } + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + if (showCheckbox) { + Checkbox( + state = if (item.checked) ToggleableState.On else ToggleableState.Off, + onClick = { + onCheckboxChange?.invoke(item.id, !item.checked) + }, + enabled = item.checkboxEnabled + ) + Spacer(Modifier.padding(horizontal = 4.dp)) + } + IconButton( + onClick = {}, + modifier = Modifier + .draggableHandle( + onDragStarted = { + haptics.longPress() + }, + onDragStopped = { + haptics.confirm() + }, + ), + ) { + Icon( + Icons.Rounded.DragIndicator, + contentDescription = "拖动排序", + tint = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) + } + } + } + } + } + } + } + } + + Orientation.Row -> { + ReorderableRow( + list = items, + modifier = modifier.fillMaxHeight(), + horizontalArrangement = Arrangement.spacedBy(UiSpacing.Small), + onSettle = onSettle, + onMove = haptics.segmentTick, + ) { _, item, _ -> + key(item.id) { + ReorderableItem { + Card( + modifier = Modifier.fillMaxHeight(), + ) { + Column( + modifier = Modifier + .padding( + horizontal = UiSpacing.CardTitle, + vertical = UiSpacing.FieldLabelBottom + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + if (showCheckbox) { + Checkbox( + state = if (item.checked) ToggleableState.On else ToggleableState.Off, + onClick = { + onCheckboxChange?.invoke(item.id, !item.checked) + }, + enabled = item.checkboxEnabled + ) + Spacer(Modifier.padding(horizontal = 4.dp)) + } + IconButton( + onClick = {}, + modifier = Modifier + .draggableHandle( + onDragStarted = { + haptics.longPress() + }, + onDragStopped = { + haptics.confirm() + }, + ), + ) { + Icon( + Icons.Rounded.DragIndicator, + contentDescription = "拖动排序", + tint = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) + } + } + Spacer(Modifier.padding(UiSpacing.CardContent)) + Text( + text = item.title, + color = MiuixTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + ) + if (item.subtitle.isNotBlank()) { + Text( + text = item.subtitle, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + fontSize = 13.sp, + ) + } + } + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/SortableCardList.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/SortableCardList.kt deleted file mode 100644 index 247b39f..0000000 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/SortableCardList.kt +++ /dev/null @@ -1,158 +0,0 @@ -package io.github.miuzarte.scrcpyforandroid.widgets - -import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DragIndicator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp -import androidx.compose.ui.zIndex -import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing -import top.yukonga.miuix.kmp.basic.Card -import top.yukonga.miuix.kmp.basic.Icon -import top.yukonga.miuix.kmp.basic.Text -import top.yukonga.miuix.kmp.theme.MiuixTheme - -enum class SortTransferDirection { - NONE, - TO_LEFT, - TO_RIGHT, -} - -data class SortableCardItem( - val id: String, - val title: String, - val subtitle: String = "", -) - -data class SortDropPayload( - val itemId: String, - val deltaX: Float, - val deltaY: Float, - val transferDirection: SortTransferDirection, -) - -/** - * Displays a titled list of sortable cards. - * - * Each item can be long-pressed and dragged. When a drag ends the - * `onDrop` callback receives a payload describing the item id and - * the drag delta which callers can use to perform reordering or - * transfer actions. - * - * @param title The section title displayed above the list. - * @param items The list of items to render as sortable cards. - * @param modifier Optional [Modifier] applied to the outer column. - * @param transferDirection Hint for transfer direction used by callers. - * @param onLongPressHaptic Optional callback invoked when an item is long-pressed. - * @param onDrop Callback invoked when a dragged item is dropped. - */ -@Composable -fun SortableCardList( - title: String, - items: List, - modifier: Modifier = Modifier, - transferDirection: SortTransferDirection = SortTransferDirection.NONE, - onLongPressHaptic: (() -> Unit)? = null, - onDrop: (SortDropPayload) -> Unit, -) { - Column(modifier = modifier) { - SectionSmallTitle(text = title, showLeadingSpacer = false) - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(UiSpacing.FieldLabelBottom), - ) { - items.forEach { item -> - var dragX by remember(item.id) { mutableFloatStateOf(0f) } - var dragY by remember(item.id) { mutableFloatStateOf(0f) } - var dragging by remember(item.id) { androidx.compose.runtime.mutableStateOf(false) } - - Card( - modifier = Modifier - .fillMaxWidth() - .zIndex(if (dragging) 1f else 0f) - .graphicsLayer { - translationX = if (dragging) dragX else 0f - translationY = if (dragging) dragY else 0f - } - .pointerInput(item.id) { - detectDragGesturesAfterLongPress( - onDragStart = { - dragging = true - dragX = 0f - dragY = 0f - onLongPressHaptic?.invoke() - }, - onDrag = { change, dragAmount -> - change.consume() - dragX += dragAmount.x - dragY += dragAmount.y - }, - onDragEnd = { - onDrop( - SortDropPayload( - itemId = item.id, - deltaX = dragX, - deltaY = dragY, - transferDirection = transferDirection, - ), - ) - dragging = false - dragX = 0f - dragY = 0f - }, - onDragCancel = { - dragging = false - dragX = 0f - dragY = 0f - }, - ) - }, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = UiSpacing.CardContent, - vertical = UiSpacing.FieldLabelBottom - ), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = item.title, - color = MiuixTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, - ) - if (item.subtitle.isNotBlank()) { - Text( - text = item.subtitle, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, - fontSize = 13.sp, - ) - } - } - Icon( - Icons.Default.DragIndicator, - contentDescription = "拖动排序", - tint = MiuixTheme.colorScheme.onSurfaceVariantSummary, - ) - } - } - } - } - } -} diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt index 144a11a..2d15c3b 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/StatusCardModels.kt @@ -81,7 +81,7 @@ internal fun StatusCardLayout( .fillMaxHeight(), colors = defaultColors(color = spec.big.containerColor), pressFeedbackType = PressFeedbackType.Tilt, - onClick = haptics.press, + onClick = haptics.contextClick, ) { Box(modifier = Modifier.fillMaxSize()) { Box( @@ -158,7 +158,7 @@ private fun StatusMetricCard(spec: StatusSmallCardSpec, modifier: Modifier) { modifier = modifier, insideMargin = PaddingValues(UiSpacing.Large), pressFeedbackType = PressFeedbackType.Tilt, - onClick = haptics.press, + onClick = haptics.contextClick, ) { Text( text = spec.title, diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt index c764646..9756d19 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/widgets/VirtualButtons.kt @@ -4,21 +4,23 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box 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.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.VolumeDown -import androidx.compose.material.icons.automirrored.filled.VolumeOff -import androidx.compose.material.icons.automirrored.filled.VolumeUp -import androidx.compose.material.icons.filled.Apps -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Notifications -import androidx.compose.material.icons.filled.PhotoCamera -import androidx.compose.material.icons.filled.PowerSettingsNew +import androidx.compose.material.icons.automirrored.rounded.VolumeDown +import androidx.compose.material.icons.automirrored.rounded.VolumeOff +import androidx.compose.material.icons.automirrored.rounded.VolumeUp +import androidx.compose.material.icons.rounded.Apps +import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.Menu +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material.icons.rounded.Notifications +import androidx.compose.material.icons.rounded.PowerSettingsNew +import androidx.compose.material.icons.rounded.Screenshot import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -28,15 +30,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp +import io.github.miuzarte.scrcpyforandroid.constants.AppDefaults import io.github.miuzarte.scrcpyforandroid.constants.UiAndroidKeycodes import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing +import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults -import top.yukonga.miuix.kmp.basic.DropdownImpl import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.ListPopupColumn import top.yukonga.miuix.kmp.basic.ListPopupDefaults import top.yukonga.miuix.kmp.basic.PopupPositionProvider +import top.yukonga.miuix.kmp.basic.SpinnerDefaults +import top.yukonga.miuix.kmp.basic.SpinnerEntry +import top.yukonga.miuix.kmp.basic.SpinnerItemImpl import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.extra.SuperListPopup import top.yukonga.miuix.kmp.theme.MiuixTheme @@ -50,13 +56,13 @@ enum class VirtualButtonAction( MORE( "more", "更多", - Icons.Default.MoreVert, + Icons.Rounded.MoreVert, null ), HOME( "home", "主页", - Icons.Default.Home, + Icons.Rounded.Home, UiAndroidKeycodes.HOME ), BACK( @@ -68,97 +74,87 @@ enum class VirtualButtonAction( APP_SWITCH( "app_switch", "多任务", - Icons.Default.Apps, + Icons.Rounded.Apps, UiAndroidKeycodes.APP_SWITCH ), MENU( "menu", "菜单", - Icons.Default.Menu, + Icons.Rounded.Menu, UiAndroidKeycodes.MENU ), NOTIFICATION( "notification", "通知栏", - Icons.Default.Notifications, + Icons.Rounded.Notifications, UiAndroidKeycodes.NOTIFICATION ), VOLUME_UP( "volume_up", "音量+", - Icons.AutoMirrored.Filled.VolumeUp, + Icons.AutoMirrored.Rounded.VolumeUp, UiAndroidKeycodes.VOLUME_UP ), VOLUME_DOWN( "volume_down", "音量-", - Icons.AutoMirrored.Filled.VolumeDown, + Icons.AutoMirrored.Rounded.VolumeDown, UiAndroidKeycodes.VOLUME_DOWN ), VOLUME_MUTE( "volume_mute", "静音", - Icons.AutoMirrored.Filled.VolumeOff, + Icons.AutoMirrored.Rounded.VolumeOff, UiAndroidKeycodes.VOLUME_MUTE ), POWER( "power", "锁屏", - Icons.Default.PowerSettingsNew, + Icons.Rounded.PowerSettingsNew, UiAndroidKeycodes.POWER ), SCREENSHOT( "screenshot", "截图", - Icons.Default.PhotoCamera, + Icons.Rounded.Screenshot, UiAndroidKeycodes.SYSRQ ), } +data class VirtualButtonItem( + val action: VirtualButtonAction, + val showOutside: Boolean, +) + object VirtualButtonActions { val all = VirtualButtonAction.entries private val byId = all.associateBy { it.id } - fun parseStoredIds(raw: String): List { - if (raw.isBlank()) return emptyList() + fun parseStoredLayout(raw: String): List { + if (raw.isBlank()) + return parseStoredLayout(AppDefaults.VIRTUAL_BUTTONS_LAYOUT) + return raw.split(',').mapNotNull { item -> - val id = item.trim() - id.ifBlank { null } + val parts = item.trim().split(':') + if (parts.size != 2) return@mapNotNull null + val id = parts[0] + val showOutside = parts[1] == "1" + val action = byId[id] ?: return@mapNotNull null + VirtualButtonItem(action, showOutside) } } - fun encodeStoredIds(ids: List): String = ids.joinToString(",") - - fun resolveLayout( - outsideIds: List, - moreIds: List, - ): Pair, List> { - val outside = mutableListOf() - val overflow = mutableListOf() - val used = mutableSetOf() - - outsideIds.forEach { id -> - val action = byId[id] ?: return@forEach - if (used.add(action.id)) outside.add(action) + fun encodeStoredLayout(items: List): String { + return items.joinToString(",") { item -> + "${item.action.id}:${if (item.showOutside) "1" else "0"}" } + } - if (used.add(VirtualButtonAction.MORE.id)) { - outside.add(VirtualButtonAction.MORE) - } - - moreIds.forEach { id -> - if (id == VirtualButtonAction.MORE.id) return@forEach - val action = byId[id] ?: return@forEach - if (used.add(action.id)) overflow.add(action) - } - - all.forEach { action -> - if (action == VirtualButtonAction.MORE) return@forEach - if (used.add(action.id)) overflow.add(action) - } - - return outside to overflow + fun splitLayout(items: List): Pair, List> { + val outside = items.filter { it.showOutside }.map { it.action } + val more = items.filter { !it.showOutside }.map { it.action } + return outside to more } } @@ -169,11 +165,11 @@ class VirtualButtonBar( @Composable fun Preview( enabled: Boolean, - onPressHaptic: () -> Unit, - onConfirmHaptic: () -> Unit, + showText: Boolean, onAction: (VirtualButtonAction) -> Unit, modifier: Modifier = Modifier, ) { + val haptics = rememberAppHaptics() val activeContainerColor = MiuixTheme.colorScheme.primary val disabledContainerColor = MiuixTheme.colorScheme.primary.copy(alpha = 0.35f) val activeContentColor = MiuixTheme.colorScheme.onPrimary @@ -188,7 +184,7 @@ class VirtualButtonBar( Box(modifier = Modifier.weight(1f)) { Button( onClick = { - onPressHaptic() + haptics.contextClick() if (action == VirtualButtonAction.MORE) { showMorePopup = true } else { @@ -201,6 +197,7 @@ class VirtualButtonBar( color = activeContainerColor, disabledColor = disabledContainerColor, ), + insideMargin = PaddingValues(0.dp), ) { val contentColor = if (enabled) activeContentColor else disabledContentColor Icon( @@ -209,15 +206,16 @@ class VirtualButtonBar( modifier = Modifier.size(18.dp), tint = contentColor, ) - androidx.compose.foundation.layout.Spacer(Modifier.width(UiSpacing.Small)) - Text(action.title, color = contentColor) + if (showText) { + Spacer(Modifier.width(UiSpacing.Small)) + Text(action.title, color = contentColor) + } } if (action == VirtualButtonAction.MORE) { MorePopup( show = showMorePopup, moreActions = moreActions, onDismiss = { showMorePopup = false }, - onConfirmHaptic = onConfirmHaptic, onAction = { onAction(it) showMorePopup = false @@ -232,11 +230,10 @@ class VirtualButtonBar( @Composable fun Fullscreen( - onPressHaptic: () -> Unit, - onConfirmHaptic: () -> Unit, onAction: (VirtualButtonAction) -> Unit, modifier: Modifier = Modifier, ) { + val haptics = rememberAppHaptics() var showMorePopup by remember { mutableStateOf(false) } Row( @@ -247,7 +244,7 @@ class VirtualButtonBar( Box(modifier = Modifier.weight(1f)) { Button( onClick = { - onPressHaptic() + haptics.contextClick() if (action == VirtualButtonAction.MORE) { showMorePopup = true } else { @@ -270,7 +267,6 @@ class VirtualButtonBar( show = showMorePopup, moreActions = moreActions, onDismiss = { showMorePopup = false }, - onConfirmHaptic = onConfirmHaptic, onAction = { onAction(it) showMorePopup = false @@ -288,10 +284,26 @@ class VirtualButtonBar( show: Boolean, moreActions: List, onDismiss: () -> Unit, - onConfirmHaptic: () -> Unit, onAction: (VirtualButtonAction) -> Unit, renderInRootScaffold: Boolean, ) { + val haptics = rememberAppHaptics() + val spinnerItems = remember(moreActions) { + moreActions.map { action -> + SpinnerEntry( + icon = { + Icon( + action.icon, + contentDescription = action.title, + modifier = Modifier + .padding(end = UiSpacing.CardContent), + ) + }, + title = action.title, + ) + } + } + SuperListPopup( show = show, popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, @@ -301,15 +313,17 @@ class VirtualButtonBar( enableWindowDim = false, ) { ListPopupColumn { - moreActions.forEachIndexed { index, action -> - DropdownImpl( - text = action.title, - optionSize = moreActions.size, + spinnerItems.forEachIndexed { index, entry -> + SpinnerItemImpl( + entry = entry, + entryCount = spinnerItems.size, isSelected = false, index = index, - onSelectedIndexChange = { - onConfirmHaptic() - onAction(action) + spinnerColors = SpinnerDefaults.spinnerColors(), + dialogMode = false, + onSelectedIndexChange = { selectedIdx -> + haptics.confirm() + onAction(moreActions[selectedIdx]) }, ) }