enhance: Settings and Virtual Button Management

- 用 `reorderable` 重构排序组件
- 重构虚拟按钮配置储存逻辑
- 虚拟图标现在可以选择是否显示文字描述
- 统一所有图标风格为 `Rounded`
- `StatusCard` 状态卡大卡的颜色现在优先取决于应用内设置,而非系统设置
This commit is contained in:
Miuzarte
2026-03-22 17:32:24 +08:00
parent 4c7cf64b98
commit 57d2af638a
18 changed files with 598 additions and 502 deletions

View File

@@ -11,9 +11,10 @@
<img src="https://github.com/user-attachments/assets/74170ada-6dee-4ec7-ab24-c5ef2a231a47" height="320" alt="Screenshot 2" /> <img src="https://github.com/user-attachments/assets/74170ada-6dee-4ec7-ab24-c5ef2a231a47" height="320" alt="Screenshot 2" />
<img src="https://github.com/user-attachments/assets/6301f2fb-624b-4209-b548-6f37b9bcedc8" height="320" alt="Screenshot 3" /> <img src="https://github.com/user-attachments/assets/6301f2fb-624b-4209-b548-6f37b9bcedc8" height="320" alt="Screenshot 3" />
<img src="https://github.com/user-attachments/assets/f513b7ba-0389-4176-8382-c1a08c4eba99" height="320" alt="Screenshot 4" /> <img src="https://github.com/user-attachments/assets/f513b7ba-0389-4176-8382-c1a08c4eba99" height="320" alt="Screenshot 4" />
<img src="https://github.com/user-attachments/assets/7a50bd1f-8095-4269-8e58-88316d86e3d8" height="320" alt="Screenshot 5" />
</p> </p>
<p align="center"> <p align="center">
<img src="https://github.com/user-attachments/assets/1b52a662-108d-49cb-a86e-eccc8ac12d64" height="160" alt="Screenshot 5" /> <img src="https://github.com/user-attachments/assets/1b52a662-108d-49cb-a86e-eccc8ac12d64" height="160" alt="Screenshot 6" />
</p> </p>
## Features ## Features
@@ -24,10 +25,11 @@
## 已知问题 / TODO ## 已知问题 / TODO
- 组件排序动画一坨
- 退出全屏时的横竖屏状态可能不对,断开 scrcpy 重连就好 - 退出全屏时的横竖屏状态可能不对,断开 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 - I18N
## 建议搭配模块 ## 建议搭配模块

View File

@@ -12,6 +12,10 @@
"name": "miuix", "name": "miuix",
"path": "../miuix" "path": "../miuix"
}, },
{
"name": "Reorderable",
"path": "../Reorderable"
},
{ {
"name": "shizuku", "name": "shizuku",
"path": "../shizuku" "path": "../shizuku"

View File

@@ -96,6 +96,7 @@ dependencies {
implementation("org.lsposed.libcxx:libcxx:27.0.12077973") implementation("org.lsposed.libcxx:libcxx:27.0.12077973")
implementation("org.bouncycastle:bcpkix-jdk18on:1.80") implementation("org.bouncycastle:bcpkix-jdk18on:1.80")
implementation("org.conscrypt:conscrypt-android:2.5.2") implementation("org.conscrypt:conscrypt-android:2.5.2")
implementation("sh.calvin.reorderable:reorderable:3.0.0")
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(platform(libs.androidx.compose.bom))

View File

@@ -65,9 +65,9 @@ object AppDefaults {
const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = true const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = true
const val KEEP_SCREEN_ON_WHEN_STREAMING = false const val KEEP_SCREEN_ON_WHEN_STREAMING = false
const val DEVICE_PREVIEW_CARD_HEIGHT_DP = 320 const val DEVICE_PREVIEW_CARD_HEIGHT_DP = 320
const val VIRTUAL_BUTTONS_OUTSIDE = "more,home,back" const val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = true
const val VIRTUAL_BUTTONS_IN_MORE = const val VIRTUAL_BUTTONS_LAYOUT =
"app_switch,menu,notification,volume_up,volume_down,volume_mute,power,screenshot" "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 = "" const val CUSTOM_SERVER_URI = ""

View File

@@ -67,8 +67,8 @@ object AppPreferenceKeys {
const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = "show_fullscreen_virtual_buttons" const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = "show_fullscreen_virtual_buttons"
const val KEEP_SCREEN_ON_WHEN_STREAMING = "keep_screen_on_when_streaming" 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 DEVICE_PREVIEW_CARD_HEIGHT_DP = "device_preview_card_height_dp"
const val VIRTUAL_BUTTONS_OUTSIDE = "virtual_buttons_outside" const val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = "preview_virtual_button_show_text"
const val VIRTUAL_BUTTONS_IN_MORE = "virtual_buttons_in_more" const val VIRTUAL_BUTTONS_LAYOUT = "virtual_buttons_layout"
const val CUSTOM_SERVER_URI = "custom_server_uri" const val CUSTOM_SERVER_URI = "custom_server_uri"

View File

@@ -7,26 +7,87 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
@Composable
fun performHaptics(type: HapticFeedbackType): Unit {
LocalHapticFeedback.current.performHapticFeedback(type)
}
@Immutable @Immutable
data class AppHaptics( data class AppHaptics(
val press: () -> Unit,
val confirm: () -> 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 @Composable
fun rememberAppHaptics(): AppHaptics { fun rememberAppHaptics(): AppHaptics {
val hapticFeedback = LocalHapticFeedback.current val hapticFeedback = LocalHapticFeedback.current
val pressAction = rememberUpdatedState {
val performConfirm = rememberUpdatedState {
hapticFeedback.performHapticFeedback(HapticFeedbackType.Confirm)
}
val performContextClick = rememberUpdatedState {
hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick) hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick)
} }
val confirmAction = rememberUpdatedState { val performGestureEnd = rememberUpdatedState {
hapticFeedback.performHapticFeedback(HapticFeedbackType.Confirm) 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 { return remember {
AppHaptics( AppHaptics(
press = { pressAction.value.invoke() }, confirm = { performConfirm.value.invoke() },
confirm = { confirmAction.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() },
) )
} }
} }

View File

@@ -5,7 +5,6 @@ import android.util.Log
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect 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.PairingCard
import io.github.miuzarte.scrcpyforandroid.widgets.PreviewCard import io.github.miuzarte.scrcpyforandroid.widgets.PreviewCard
import io.github.miuzarte.scrcpyforandroid.widgets.QuickConnectCard 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.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.StatusCard
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
@@ -73,7 +69,6 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.roundToInt
private const val ADB_CONNECT_TIMEOUT_MS = 3_000L private const val ADB_CONNECT_TIMEOUT_MS = 3_000L
private const val ADB_KEEPALIVE_INTERVAL_MS = 3_000L private const val ADB_KEEPALIVE_INTERVAL_MS = 3_000L
@@ -125,9 +120,10 @@ fun DeviceTabScreen(
nativeCore: NativeCoreFacade, nativeCore: NativeCoreFacade,
snack: SnackbarHostState, snack: SnackbarHostState,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
virtualButtonsOutside: List<String>, virtualButtonsLayout: String,
virtualButtonsInMore: List<String>, showPreviewVirtualButtonText: Boolean,
previewCardHeightDp: Int, previewCardHeightDp: Int,
themeBaseIndex: Int,
customServerUri: String?, customServerUri: String?,
serverRemotePath: String, serverRemotePath: String,
onServerRemotePathChange: (String) -> Unit, onServerRemotePathChange: (String) -> Unit,
@@ -218,8 +214,8 @@ fun DeviceTabScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val haptics = rememberAppHaptics() val haptics = rememberAppHaptics()
val virtualButtonLayout = remember(virtualButtonsOutside, virtualButtonsInMore) { val virtualButtonLayout = remember(virtualButtonsLayout) {
VirtualButtonActions.resolveLayout(virtualButtonsOutside, virtualButtonsInMore) VirtualButtonActions.splitLayout(VirtualButtonActions.parseStoredLayout(virtualButtonsLayout))
} }
val initialSettings = remember(context) { loadDevicePageSettings(context) } val initialSettings = remember(context) { loadDevicePageSettings(context) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -945,34 +941,33 @@ fun DeviceTabScreen(
SuperBottomSheet( SuperBottomSheet(
show = showReorderSheet, show = showReorderSheet,
title = "调整设备排序", title = "快速设备排序",
onDismissRequest = { showReorderSheet = false }, onDismissRequest = { showReorderSheet = false },
) { ) {
SortableCardList( val list = remember {
title = "设备列表", ReorderableList(
items = quickDevices.map { device -> {
SortableCardItem( quickDevices.map { device ->
id = device.id, ReorderableList.Item(
title = device.name.ifBlank { device.host }, id = device.id,
subtitle = "${device.host}:${device.port}", title = device.name.ifBlank { device.host },
) subtitle = "${device.host}:${device.port}",
}, )
modifier = Modifier.padding(horizontal = UiSpacing.CardContent), }
transferDirection = SortTransferDirection.NONE, },
onLongPressHaptic = { haptics.press() }, onSettle = { fromIndex, toIndex ->
onDrop = { payload: SortDropPayload -> if (fromIndex < 0) return@ReorderableList
val fromIndex = quickDevices.indexOfFirst { it.id == payload.itemId } val to = toIndex.coerceIn(0, quickDevices.size)
if (fromIndex < 0) return@SortableCardList if (fromIndex == to) return@ReorderableList
val steps = (payload.deltaY / 54f).roundToInt()
if (steps == 0) return@SortableCardList val moved = quickDevices.removeAt(fromIndex)
val toIndex = (fromIndex + steps).coerceIn(0, quickDevices.lastIndex) quickDevices.add(to.coerceIn(0, quickDevices.size), moved)
if (toIndex == fromIndex) return@SortableCardList saveQuickDevices(context, quickDevices)
val moved = quickDevices.removeAt(fromIndex) },
quickDevices.add(toIndex, moved) )
saveQuickDevices(context, quickDevices) }
}, list()
) Spacer(Modifier.height(UiSpacing.BottomSheetBottom))
Spacer(Modifier.height(UiSpacing.Large))
} }
fun sendVirtualButtonAction(action: VirtualButtonAction) { fun sendVirtualButtonAction(action: VirtualButtonAction) {
@@ -1022,6 +1017,7 @@ fun DeviceTabScreen(
sessionInfo = sessionInfo, sessionInfo = sessionInfo,
busyLabel = null, busyLabel = null,
connectedDeviceLabel = connectedDeviceLabel, connectedDeviceLabel = connectedDeviceLabel,
themeBaseIndex = themeBaseIndex,
) )
} }
@@ -1043,7 +1039,7 @@ fun DeviceTabScreen(
} }
}, },
onAction = { onAction = {
haptics.press() haptics.contextClick()
if (isConnectedTarget) { if (isConnectedTarget) {
activeDeviceActionId = device.id activeDeviceActionId = device.id
runAdbConnect("断开 ADB", onFinished = { activeDeviceActionId = null }) { runAdbConnect("断开 ADB", onFinished = { activeDeviceActionId = null }) {
@@ -1114,7 +1110,7 @@ fun DeviceTabScreen(
} }
}, },
) )
SectionSmallTitle(text = "无线配对") SectionSmallTitle("无线配对")
// "使用配对码配对设备" // "使用配对码配对设备"
PairingCard( PairingCard(
busy = busy, busy = busy,
@@ -1167,7 +1163,7 @@ fun DeviceTabScreen(
audioCodec = audioCodec, audioCodec = audioCodec,
onAudioCodecChange = onAudioCodecChange, onAudioCodecChange = onAudioCodecChange,
onOpenAdvanced = onOpenAdvancedPage, onOpenAdvanced = onOpenAdvancedPage,
onStartStopHaptic = { haptics.press() }, onStartStopHaptic = { haptics.contextClick() },
onStart = { onStart = {
runBusy("启动 scrcpy") { runBusy("启动 scrcpy") {
if (noVideo && !audioEnabled) { if (noVideo && !audioEnabled) {
@@ -1316,17 +1312,13 @@ fun DeviceTabScreen(
previewHeightDp = previewCardHeightDp.coerceAtLeast(120), previewHeightDp = previewCardHeightDp.coerceAtLeast(120),
controlsVisible = previewControlsVisible, controlsVisible = previewControlsVisible,
onTapped = { onTapped = {
previewControlsVisible = true previewControlsVisible = !previewControlsVisible
scope.launch {
delay(2000)
previewControlsVisible = false
}
}, },
onOpenFullscreen = { onOpenFullscreen = {
val info = sessionInfo ?: return@PreviewCard val info = sessionInfo ?: return@PreviewCard
onOpenFullscreenPage(info) onOpenFullscreenPage(info)
}, },
onOpenFullscreenHaptic = { haptics.press() }, onOpenFullscreenHaptic = { haptics.contextClick() },
) )
} }
item { item {
@@ -1334,8 +1326,7 @@ fun DeviceTabScreen(
busy = busy, busy = busy,
outsideActions = virtualButtonLayout.first, outsideActions = virtualButtonLayout.first,
moreActions = virtualButtonLayout.second, moreActions = virtualButtonLayout.second,
onPressHaptic = { haptics.press() }, showText = showPreviewVirtualButtonText,
onConfirmHaptic = { haptics.confirm() },
onAction = ::sendVirtualButtonAction, onAction = ::sendVirtualButtonAction,
) )
} }

View File

@@ -38,8 +38,7 @@ data class FullscreenControlLaunch(
fun FullscreenControlPage( fun FullscreenControlPage(
launch: FullscreenControlLaunch, launch: FullscreenControlLaunch,
nativeCore: NativeCoreFacade, nativeCore: NativeCoreFacade,
virtualButtonsOutside: List<String>, virtualButtonsLayout: String,
virtualButtonsInMore: List<String>,
showDebugInfo: Boolean, showDebugInfo: Boolean,
showVirtualButtons: Boolean, showVirtualButtons: Boolean,
onVideoSizeChanged: (width: Int, height: Int) -> Unit, onVideoSizeChanged: (width: Int, height: Int) -> Unit,
@@ -51,8 +50,8 @@ fun FullscreenControlPage(
val context = LocalContext.current val context = LocalContext.current
val haptics = rememberAppHaptics() val haptics = rememberAppHaptics()
val activity = remember(context) { context as? Activity } val activity = remember(context) { context as? Activity }
val virtualButtonLayout = remember(virtualButtonsOutside, virtualButtonsInMore) { val virtualButtonLayout = remember(virtualButtonsLayout) {
VirtualButtonActions.resolveLayout(virtualButtonsOutside, virtualButtonsInMore) VirtualButtonActions.splitLayout(VirtualButtonActions.parseStoredLayout(virtualButtonsLayout))
} }
val bar = remember(virtualButtonLayout) { val bar = remember(virtualButtonLayout) {
VirtualButtonBar( VirtualButtonBar(
@@ -150,8 +149,6 @@ fun FullscreenControlPage(
if (showVirtualButtons) { if (showVirtualButtons) {
bar.Fullscreen( bar.Fullscreen(
modifier = Modifier.align(Alignment.BottomCenter), modifier = Modifier.align(Alignment.BottomCenter),
onPressHaptic = { haptics.press() },
onConfirmHaptic = { haptics.confirm() },
onAction = { action -> onAction = { action ->
action.keycode?.let(::sendKeycode) action.keycode?.let(::sendKeycode)
}, },

View File

@@ -19,9 +19,9 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Devices import androidx.compose.material.icons.rounded.Devices
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.rounded.Settings
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect 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.loadDevicePageSettings
import io.github.miuzarte.scrcpyforandroid.services.loadMainSettings import io.github.miuzarte.scrcpyforandroid.services.loadMainSettings
import io.github.miuzarte.scrcpyforandroid.services.saveMainSettings import io.github.miuzarte.scrcpyforandroid.services.saveMainSettings
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -82,8 +81,8 @@ private enum class MainTabDestination(
val label: String, val label: String,
val icon: ImageVector, val icon: ImageVector,
) { ) {
Device(title = "设备", label = "设备", icon = Icons.Default.Devices), Device(title = "设备", label = "设备", icon = Icons.Rounded.Devices),
Settings(title = "设置", label = "设置", icon = Icons.Default.Settings), Settings(title = "设置", label = "设置", icon = Icons.Rounded.Settings),
} }
private sealed interface RootScreen : NavKey { private sealed interface RootScreen : NavKey {
@@ -134,14 +133,10 @@ fun MainPage() {
var monetEnabled by rememberSaveable { mutableStateOf(initialSettings.monetEnabled) } var monetEnabled by rememberSaveable { mutableStateOf(initialSettings.monetEnabled) }
var fullscreenDebugInfoEnabled by rememberSaveable { mutableStateOf(initialSettings.fullscreenDebugInfoEnabled) } var fullscreenDebugInfoEnabled by rememberSaveable { mutableStateOf(initialSettings.fullscreenDebugInfoEnabled) }
var showFullscreenVirtualButtons by rememberSaveable { mutableStateOf(initialSettings.showFullscreenVirtualButtons) } var showFullscreenVirtualButtons by rememberSaveable { mutableStateOf(initialSettings.showFullscreenVirtualButtons) }
var showPreviewVirtualButtonText by rememberSaveable { mutableStateOf(initialSettings.showPreviewVirtualButtonText) }
var keepScreenOnWhenStreamingEnabled by rememberSaveable { mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled) } var keepScreenOnWhenStreamingEnabled by rememberSaveable { mutableStateOf(initialSettings.keepScreenOnWhenStreamingEnabled) }
var devicePreviewCardHeightDp by rememberSaveable { mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) } var devicePreviewCardHeightDp by rememberSaveable { mutableIntStateOf(initialSettings.devicePreviewCardHeightDp) }
var virtualButtonsOutside by rememberSaveable(stateSaver = stringListSaver) { var virtualButtonsLayout by rememberSaveable { mutableStateOf(initialSettings.virtualButtonsLayout) }
mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsOutside))
}
var virtualButtonsInMore by rememberSaveable(stateSaver = stringListSaver) {
mutableStateOf(VirtualButtonActions.parseStoredIds(initialSettings.virtualButtonsInMore))
}
var customServerUri by rememberSaveable { mutableStateOf(initialSettings.customServerUri) } var customServerUri by rememberSaveable { mutableStateOf(initialSettings.customServerUri) }
var serverRemotePath by rememberSaveable { mutableStateOf(initialSettings.serverRemotePath) } var serverRemotePath by rememberSaveable { mutableStateOf(initialSettings.serverRemotePath) }
var adbKeyName by rememberSaveable { mutableStateOf(initialSettings.adbKeyName) } var adbKeyName by rememberSaveable { mutableStateOf(initialSettings.adbKeyName) }
@@ -241,10 +236,10 @@ fun MainPage() {
monetEnabled, monetEnabled,
fullscreenDebugInfoEnabled, fullscreenDebugInfoEnabled,
showFullscreenVirtualButtons, showFullscreenVirtualButtons,
showPreviewVirtualButtonText,
keepScreenOnWhenStreamingEnabled, keepScreenOnWhenStreamingEnabled,
devicePreviewCardHeightDp, devicePreviewCardHeightDp,
virtualButtonsOutside, virtualButtonsLayout,
virtualButtonsInMore,
customServerUri, customServerUri,
serverRemotePath, serverRemotePath,
adbKeyName, adbKeyName,
@@ -262,10 +257,10 @@ fun MainPage() {
monetEnabled = monetEnabled, monetEnabled = monetEnabled,
fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled, fullscreenDebugInfoEnabled = fullscreenDebugInfoEnabled,
showFullscreenVirtualButtons = showFullscreenVirtualButtons, showFullscreenVirtualButtons = showFullscreenVirtualButtons,
showPreviewVirtualButtonText = showPreviewVirtualButtonText,
keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled, keepScreenOnWhenStreamingEnabled = keepScreenOnWhenStreamingEnabled,
devicePreviewCardHeightDp = devicePreviewCardHeightDp, devicePreviewCardHeightDp = devicePreviewCardHeightDp,
virtualButtonsOutside = VirtualButtonActions.encodeStoredIds(virtualButtonsOutside), virtualButtonsLayout = virtualButtonsLayout,
virtualButtonsInMore = VirtualButtonActions.encodeStoredIds(virtualButtonsInMore),
customServerUri = customServerUri, customServerUri = customServerUri,
serverRemotePath = serverRemotePath, serverRemotePath = serverRemotePath,
adbKeyName = adbKeyName, adbKeyName = adbKeyName,
@@ -398,7 +393,7 @@ fun MainPage() {
holdDownState = showDeviceMenu, holdDownState = showDeviceMenu,
) { ) {
Icon( Icon(
Icons.Default.MoreVert, Icons.Rounded.MoreVert,
contentDescription = "更多" contentDescription = "更多"
) )
} }
@@ -429,8 +424,10 @@ fun MainPage() {
nativeCore = nativeCore, nativeCore = nativeCore,
snack = snackHostState, snack = snackHostState,
scrollBehavior = deviceScrollBehavior, scrollBehavior = deviceScrollBehavior,
virtualButtonsOutside = virtualButtonsOutside, virtualButtonsLayout = virtualButtonsLayout,
virtualButtonsInMore = virtualButtonsInMore, showPreviewVirtualButtonText = showPreviewVirtualButtonText,
previewCardHeightDp = devicePreviewCardHeightDp,
themeBaseIndex = themeBaseIndex,
customServerUri = customServerUri, customServerUri = customServerUri,
serverRemotePath = serverRemotePath, serverRemotePath = serverRemotePath,
onServerRemotePathChange = { serverRemotePath = it }, onServerRemotePathChange = { serverRemotePath = it },
@@ -556,7 +553,6 @@ fun MainPage() {
), ),
) )
}, },
previewCardHeightDp = devicePreviewCardHeightDp,
adbPairingAutoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen, adbPairingAutoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen,
adbAutoReconnectPairedDevice = adbAutoReconnectPairedDevice, adbAutoReconnectPairedDevice = adbAutoReconnectPairedDevice,
adbMdnsLanDiscoveryEnabled = adbMdnsLanDiscoveryEnabled, adbMdnsLanDiscoveryEnabled = adbMdnsLanDiscoveryEnabled,
@@ -589,6 +585,12 @@ fun MainPage() {
onDevicePreviewCardHeightDpChange = { onDevicePreviewCardHeightDpChange = {
devicePreviewCardHeightDp = it.coerceAtLeast(120) devicePreviewCardHeightDp = it.coerceAtLeast(120)
}, },
onOpenReorderDevices = {
openReorderDevicesAction?.invoke()
},
onOpenVirtualButtonOrder = {
rootBackStack.add(RootScreen.VirtualButtonOrder)
},
showFullscreenVirtualButtons = showFullscreenVirtualButtons, showFullscreenVirtualButtons = showFullscreenVirtualButtons,
onShowFullscreenVirtualButtonsChange = { onShowFullscreenVirtualButtonsChange = {
showFullscreenVirtualButtons = it showFullscreenVirtualButtons = it
@@ -756,12 +758,12 @@ fun MainPage() {
VirtualButtonOrderPage( VirtualButtonOrderPage(
contentPadding = pagePadding, contentPadding = pagePadding,
scrollBehavior = advancedScrollBehavior, scrollBehavior = advancedScrollBehavior,
outsideIds = virtualButtonsOutside, layoutString = virtualButtonsLayout,
moreIds = virtualButtonsInMore, onLayoutChange = { layout ->
onLayoutChange = { outside, more -> virtualButtonsLayout = layout
virtualButtonsOutside = outside
virtualButtonsInMore = more
}, },
showPreviewText = showPreviewVirtualButtonText,
onShowPreviewTextChange = { showPreviewVirtualButtonText = it },
) )
} }
} }
@@ -770,8 +772,7 @@ fun MainPage() {
FullscreenControlPage( FullscreenControlPage(
launch = screen.launch, launch = screen.launch,
nativeCore = nativeCore, nativeCore = nativeCore,
virtualButtonsOutside = virtualButtonsOutside, virtualButtonsLayout = virtualButtonsLayout,
virtualButtonsInMore = virtualButtonsInMore,
showDebugInfo = fullscreenDebugInfoEnabled, showDebugInfo = fullscreenDebugInfoEnabled,
showVirtualButtons = showFullscreenVirtualButtons, showVirtualButtons = showFullscreenVirtualButtons,
onVideoSizeChanged = { width, height -> onVideoSizeChanged = { width, height ->
@@ -817,7 +818,7 @@ private fun DeviceMenuPopup(
) { ) {
ListPopupColumn { ListPopupColumn {
DeviceMenuPopupItem( DeviceMenuPopupItem(
text = "调整设备排序", text = "快速设备排序",
optionSize = 3, optionSize = 3,
index = 0, index = 0,
onClick = onReorderDevices, onClick = onReorderDevices,

View File

@@ -1,5 +1,6 @@
package io.github.miuzarte.scrcpyforandroid.pages package io.github.miuzarte.scrcpyforandroid.pages
import android.content.Intent
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -7,11 +8,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.rounded.FileOpen
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight 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.AppDefaults
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn 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.ScrollBehavior
import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.extra.SuperArrow
import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.extra.SuperDropdown
import top.yukonga.miuix.kmp.extra.SuperSwitch import top.yukonga.miuix.kmp.extra.SuperSwitch
import top.yukonga.miuix.kmp.theme.ColorSchemeMode import top.yukonga.miuix.kmp.theme.ColorSchemeMode
@@ -66,6 +70,8 @@ fun SettingsScreen(
devicePreviewCardHeightDp: Int, devicePreviewCardHeightDp: Int,
onDevicePreviewCardHeightDpChange: (Int) -> Unit, onDevicePreviewCardHeightDpChange: (Int) -> Unit,
showFullscreenVirtualButtons: Boolean, showFullscreenVirtualButtons: Boolean,
onOpenReorderDevices: () -> Unit,
onOpenVirtualButtonOrder: () -> Unit,
onShowFullscreenVirtualButtonsChange: (Boolean) -> Unit, onShowFullscreenVirtualButtonsChange: (Boolean) -> Unit,
customServerUri: String?, customServerUri: String?,
onPickServer: () -> Unit, onPickServer: () -> Unit,
@@ -81,6 +87,7 @@ fun SettingsScreen(
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
) { ) {
val baseModeItems = THEME_BASE_OPTIONS.map { it.label } val baseModeItems = THEME_BASE_OPTIONS.map { it.label }
val context = LocalContext.current
// 设置 // 设置
AppPageLazyColumn( AppPageLazyColumn(
@@ -88,7 +95,7 @@ fun SettingsScreen(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) { ) {
item { item {
SectionSmallTitle(text = "主题") SectionSmallTitle("主题")
Card { Card {
SuperDropdown( SuperDropdown(
title = "外观模式", title = "外观模式",
@@ -105,7 +112,7 @@ fun SettingsScreen(
) )
} }
SectionSmallTitle(text = "投屏") SectionSmallTitle("投屏")
Card { Card {
SuperSwitch( SuperSwitch(
title = "启用调试信息", title = "启用调试信息",
@@ -114,8 +121,8 @@ fun SettingsScreen(
onCheckedChange = onFullscreenDebugInfoEnabledChange, onCheckedChange = onFullscreenDebugInfoEnabledChange,
) )
SuperSwitch( SuperSwitch(
title = "投屏时不允许息屏", title = "投屏时保持屏幕常亮",
summary = "Scrcpy 启动后保持本机常亮,避免锁屏导致 ADB 断开", summary = "Scrcpy 启动后保持本机屏幕常亮,避免锁屏导致 ADB 断开",
checked = keepScreenOnWhenStreamingEnabled, checked = keepScreenOnWhenStreamingEnabled,
onCheckedChange = onKeepScreenOnWhenStreamingEnabledChange, onCheckedChange = onKeepScreenOnWhenStreamingEnabledChange,
) )
@@ -140,6 +147,16 @@ fun SettingsScreen(
?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) } ?.let { onDevicePreviewCardHeightDpChange(it.coerceAtLeast(120)) }
}, },
) )
SuperArrow(
title = "快速设备排序",
summary = "手动排序设备页的快速设备",
onClick = onOpenReorderDevices,
)
SuperArrow(
title = "虚拟按钮排序",
summary = "手动排序预览/全屏时的虚拟按钮,并选择哪些按钮展示在外",
onClick = onOpenVirtualButtonOrder,
)
SuperSwitch( SuperSwitch(
title = "全屏显示虚拟按钮", title = "全屏显示虚拟按钮",
summary = "在全屏控制页底部显示返回键、主页键等虚拟按钮", summary = "在全屏控制页底部显示返回键、主页键等虚拟按钮",
@@ -148,7 +165,7 @@ fun SettingsScreen(
) )
} }
SectionSmallTitle(text = "scrcpy-server") SectionSmallTitle("scrcpy-server")
Card { Card {
Spacer(modifier = Modifier.padding(top = UiSpacing.CardContent)) Spacer(modifier = Modifier.padding(top = UiSpacing.CardContent))
Text( Text(
@@ -171,10 +188,10 @@ fun SettingsScreen(
trailingIcon = { trailingIcon = {
Row(modifier = Modifier.padding(end = UiSpacing.SectionTitleLeadingGap)) { Row(modifier = Modifier.padding(end = UiSpacing.SectionTitleLeadingGap)) {
if (customServerUri != null) IconButton(onClick = onClearServer) { if (customServerUri != null) IconButton(onClick = onClearServer) {
Icon(Icons.Default.Clear, contentDescription = "清空") Icon(Icons.Rounded.Clear, contentDescription = "清空")
} }
IconButton(onClick = onPickServer) { 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 { Card {
Text( Text(
text = "自定义 ADB 密钥名", text = "自定义 ADB 密钥名",
@@ -232,6 +249,21 @@ fun SettingsScreen(
onCheckedChange = onAdbAutoReconnectPairedDeviceChange, 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] 里 // TODO: 放进 [AppPageLazyColumn] 里

View File

@@ -1,169 +1,86 @@
package io.github.miuzarte.scrcpyforandroid.pages package io.github.miuzarte.scrcpyforandroid.pages
import androidx.compose.foundation.layout.PaddingValues 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.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.remember
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
import io.github.miuzarte.scrcpyforandroid.haptics.rememberAppHaptics
import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn import io.github.miuzarte.scrcpyforandroid.scaffolds.AppPageLazyColumn
import io.github.miuzarte.scrcpyforandroid.widgets.SortDropPayload import io.github.miuzarte.scrcpyforandroid.widgets.ReorderableList
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.VirtualButtonAction import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonAction
import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions import io.github.miuzarte.scrcpyforandroid.widgets.VirtualButtonActions
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.ScrollBehavior
import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.extra.SuperSwitch
import kotlin.math.roundToInt
private const val LIST_REORDER_STEP_PX = 54f
private const val LIST_TRANSFER_STEP_PX = 72f
@Composable @Composable
internal fun VirtualButtonOrderPage( internal fun VirtualButtonOrderPage(
contentPadding: PaddingValues, contentPadding: PaddingValues,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
outsideIds: List<String>, layoutString: String,
moreIds: List<String>, onLayoutChange: (String) -> Unit,
onLayoutChange: (outside: List<String>, more: List<String>) -> Unit, showPreviewText: Boolean,
onShowPreviewTextChange: (Boolean) -> Unit,
) { ) {
val haptics = rememberAppHaptics() var buttonItems by remember(layoutString) {
val normalized = remember(outsideIds, moreIds) { mutableStateOf(VirtualButtonActions.parseStoredLayout(layoutString))
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 })
} }
fun emit() { fun emitChanges() {
onLayoutChange(outsideState.toList(), moreState.toList()) onLayoutChange(VirtualButtonActions.encodeStoredLayout(buttonItems))
}
fun reorderInside(
list: androidx.compose.runtime.snapshots.SnapshotStateList<String>,
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<String>,
to: androidx.compose.runtime.snapshots.SnapshotStateList<String>,
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)
}
} }
AppPageLazyColumn( AppPageLazyColumn(
contentPadding = contentPadding, contentPadding = contentPadding,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) { ) {
// 按钮显示文本开关
item { item {
Card(modifier = Modifier.fillMaxWidth()) { Card {
Text( SuperSwitch(
text = "长按可在两列间拖动排序\n“更多”不可移入右侧菜单", title = "按钮显示文本",
modifier = Modifier.padding(UiSpacing.CardContent), summary = "超过3个建议关闭只对预览卡下方的虚拟按钮生效",
checked = showPreviewText,
onCheckedChange = onShowPreviewTextChange,
) )
} }
} }
item { item {
Row( ReorderableList(
modifier = Modifier.fillMaxWidth(), itemsProvider = {
) { buttonItems.map { item ->
SortableCardList( val action = item.action
title = "外部按钮", ReorderableList.Item(
items = outsideState.map { id ->
val action = VirtualButtonAction.entries.first { it.id == id }
SortableCardItem(
id = action.id, id = action.id,
title = action.title, icon = action.icon,
subtitle = "显示在预览与全屏底部", 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) orientation = ReorderableList.Orientation.Column,
.padding(end = UiSpacing.Small), onSettle = { fromIndex, toIndex ->
transferDirection = SortTransferDirection.TO_RIGHT, buttonItems = buttonItems.toMutableList().apply {
onLongPressHaptic = haptics.press, add(toIndex, removeAt(fromIndex))
onDrop = ::handleOutsideDrop, }
) emitChanges()
},
SortableCardList( showCheckbox = true,
title = "更多菜单", onCheckboxChange = { id, checked ->
items = moreState.map { id -> buttonItems = buttonItems.map { item ->
val action = VirtualButtonAction.entries.first { it.id == id } if (item.action.id == id) {
SortableCardItem( item.copy(showOutside = checked)
id = action.id, } else {
title = action.title, item
subtitle = "显示在“更多”弹窗中", }
) }
}, emitChanges()
modifier = Modifier },
.weight(1f) )()
.padding(start = UiSpacing.Small),
transferDirection = SortTransferDirection.TO_LEFT,
onLongPressHaptic = haptics.press,
onDrop = ::handleMoreDrop,
)
}
} }
} }
} }

View File

@@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope 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.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -22,6 +24,7 @@ fun AppPageLazyColumn(
contentPadding: PaddingValues, contentPadding: PaddingValues,
scrollBehavior: ScrollBehavior, scrollBehavior: ScrollBehavior,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
itemSpacing: Dp = UiSpacing.PageItem, itemSpacing: Dp = UiSpacing.PageItem,
horizontalPadding: Dp = UiSpacing.PageHorizontal, horizontalPadding: Dp = UiSpacing.PageHorizontal,
verticalPadding: Dp = UiSpacing.PageVertical, verticalPadding: Dp = UiSpacing.PageVertical,
@@ -44,6 +47,7 @@ fun AppPageLazyColumn(
.overScrollVertical() .overScrollVertical()
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection)
.padding(contentPadding), .padding(contentPadding),
state = state,
contentPadding = PaddingValues(horizontal = horizontalPadding, vertical = verticalPadding), contentPadding = PaddingValues(horizontal = horizontalPadding, vertical = verticalPadding),
verticalArrangement = Arrangement.spacedBy(itemSpacing), verticalArrangement = Arrangement.spacedBy(itemSpacing),
content = content, content = content,

View File

@@ -13,10 +13,10 @@ internal data class MainSettings(
val monetEnabled: Boolean = AppDefaults.MONET, val monetEnabled: Boolean = AppDefaults.MONET,
val fullscreenDebugInfoEnabled: Boolean = AppDefaults.FULLSCREEN_DEBUG_INFO, val fullscreenDebugInfoEnabled: Boolean = AppDefaults.FULLSCREEN_DEBUG_INFO,
val showFullscreenVirtualButtons: Boolean = AppDefaults.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, 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 keepScreenOnWhenStreamingEnabled: Boolean = AppDefaults.KEEP_SCREEN_ON_WHEN_STREAMING,
val devicePreviewCardHeightDp: Int = AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP, val devicePreviewCardHeightDp: Int = AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP,
val virtualButtonsOutside: String = AppDefaults.VIRTUAL_BUTTONS_OUTSIDE, val virtualButtonsLayout: String = AppDefaults.VIRTUAL_BUTTONS_LAYOUT,
val virtualButtonsInMore: String = AppDefaults.VIRTUAL_BUTTONS_IN_MORE,
val customServerUri: String? = AppDefaults.CUSTOM_SERVER_URI, val customServerUri: String? = AppDefaults.CUSTOM_SERVER_URI,
val serverRemotePath: String = AppDefaults.SERVER_REMOTE_PATH_INPUT, val serverRemotePath: String = AppDefaults.SERVER_REMOTE_PATH_INPUT,
val adbKeyName: String = AppDefaults.ADB_KEY_NAME_INPUT, val adbKeyName: String = AppDefaults.ADB_KEY_NAME_INPUT,
@@ -101,6 +101,10 @@ internal fun loadMainSettings(context: Context): MainSettings {
AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
AppDefaults.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( keepScreenOnWhenStreamingEnabled = prefs.getBoolean(
AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING, AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING,
AppDefaults.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, AppPreferenceKeys.DEVICE_PREVIEW_CARD_HEIGHT_DP,
AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP, AppDefaults.DEVICE_PREVIEW_CARD_HEIGHT_DP,
).coerceAtLeast(120), ).coerceAtLeast(120),
virtualButtonsOutside = prefs.getString( virtualButtonsLayout = prefs.getString(
AppPreferenceKeys.VIRTUAL_BUTTONS_OUTSIDE, AppPreferenceKeys.VIRTUAL_BUTTONS_LAYOUT,
AppDefaults.VIRTUAL_BUTTONS_OUTSIDE, AppDefaults.VIRTUAL_BUTTONS_LAYOUT,
).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_OUTSIDE }, ).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_LAYOUT },
virtualButtonsInMore = prefs.getString(
AppPreferenceKeys.VIRTUAL_BUTTONS_IN_MORE,
AppDefaults.VIRTUAL_BUTTONS_IN_MORE,
).orEmpty().ifBlank { AppDefaults.VIRTUAL_BUTTONS_IN_MORE },
customServerUri = prefs.getString( customServerUri = prefs.getString(
AppPreferenceKeys.CUSTOM_SERVER_URI, AppPreferenceKeys.CUSTOM_SERVER_URI,
AppDefaults.CUSTOM_SERVER_URI AppDefaults.CUSTOM_SERVER_URI
@@ -177,6 +177,10 @@ internal fun saveMainSettings(context: Context, settings: MainSettings) {
AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS, AppPreferenceKeys.SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
settings.showFullscreenVirtualButtons, settings.showFullscreenVirtualButtons,
) )
.putBoolean(
AppPreferenceKeys.PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT,
settings.showPreviewVirtualButtonText,
)
.putBoolean( .putBoolean(
AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING, AppPreferenceKeys.KEEP_SCREEN_ON_WHEN_STREAMING,
settings.keepScreenOnWhenStreamingEnabled, settings.keepScreenOnWhenStreamingEnabled,
@@ -186,12 +190,8 @@ internal fun saveMainSettings(context: Context, settings: MainSettings) {
settings.devicePreviewCardHeightDp.coerceAtLeast(120), settings.devicePreviewCardHeightDp.coerceAtLeast(120),
) )
.putString( .putString(
AppPreferenceKeys.VIRTUAL_BUTTONS_OUTSIDE, AppPreferenceKeys.VIRTUAL_BUTTONS_LAYOUT,
settings.virtualButtonsOutside, settings.virtualButtonsLayout,
)
.putString(
AppPreferenceKeys.VIRTUAL_BUTTONS_IN_MORE,
settings.virtualButtonsInMore,
) )
.putString( .putString(
AppPreferenceKeys.CUSTOM_SERVER_URI, AppPreferenceKeys.CUSTOM_SERVER_URI,

View File

@@ -30,9 +30,9 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddLink import androidx.compose.material.icons.rounded.AddLink
import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.rounded.CheckCircleOutline 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.LinkOff
import androidx.compose.material.icons.rounded.Wifi import androidx.compose.material.icons.rounded.Wifi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -123,18 +123,28 @@ internal fun StatusCard(
sessionInfo: ScrcpySessionInfo?, sessionInfo: ScrcpySessionInfo?,
busyLabel: String?, busyLabel: String?,
connectedDeviceLabel: String, connectedDeviceLabel: String,
themeBaseIndex: Int,
) { ) {
val cleanStatusLine = normalizeStatusLine(statusLine) val cleanStatusLine = normalizeStatusLine(statusLine)
// 根据应用主题设置决定是否使用深色模式
val isDarkTheme = when (themeBaseIndex) {
0 -> isSystemInDarkTheme() // 跟随系统
1 -> false // 浅色
2 -> true // 深色
else -> isSystemInDarkTheme()
}
val spec = when { val spec = when {
streaming && sessionInfo != null -> { streaming && sessionInfo != null -> {
val streamCardColor = when { val streamCardColor = when {
isDynamicColor -> MiuixTheme.colorScheme.secondaryContainer isDynamicColor -> MiuixTheme.colorScheme.secondaryContainer
isSystemInDarkTheme() -> Color(0xFF1A3825) isDarkTheme -> Color(0xFF1A3825)
else -> Color(0xFFDFFAE4) else -> Color(0xFFDFFAE4)
} }
val streamTextColor = when { val streamTextColor = when {
isDynamicColor -> MiuixTheme.colorScheme.onSecondaryContainer isDynamicColor -> MiuixTheme.colorScheme.onSecondaryContainer
isSystemInDarkTheme() -> Color.White isDarkTheme -> Color.White
else -> Color(0xFF111111) else -> Color(0xFF111111)
} }
val streamIconColor = if (isDynamicColor) { val streamIconColor = if (isDynamicColor) {
@@ -300,12 +310,17 @@ internal fun PreviewCard(
) { ) {
Button( Button(
onClick = { onClick = {
onOpenFullscreenHaptic?.invoke() if (alpha > 0.1) {
onOpenFullscreen() onOpenFullscreenHaptic?.invoke()
onOpenFullscreen()
}
}, },
modifier = Modifier.alpha(alpha), modifier = Modifier.alpha(alpha),
) { ) {
Icon(Icons.Default.Fullscreen, contentDescription = "全屏") Icon(
Icons.Rounded.Fullscreen,
contentDescription = "全屏",
)
Spacer(Modifier.width(UiSpacing.SectionTitleBottom)) Spacer(Modifier.width(UiSpacing.SectionTitleBottom))
Text("全屏") Text("全屏")
} }
@@ -320,8 +335,7 @@ internal fun VirtualButtonCard(
busy: Boolean, busy: Boolean,
outsideActions: List<VirtualButtonAction>, outsideActions: List<VirtualButtonAction>,
moreActions: List<VirtualButtonAction>, moreActions: List<VirtualButtonAction>,
onPressHaptic: () -> Unit, showText: Boolean,
onConfirmHaptic: () -> Unit,
onAction: (VirtualButtonAction) -> Unit, onAction: (VirtualButtonAction) -> Unit,
) { ) {
val bar = remember(outsideActions, moreActions) { val bar = remember(outsideActions, moreActions) {
@@ -334,8 +348,7 @@ internal fun VirtualButtonCard(
Card { Card {
bar.Preview( bar.Preview(
enabled = !busy, enabled = !busy,
onPressHaptic = onPressHaptic, showText = showText,
onConfirmHaptic = onConfirmHaptic,
onAction = onAction, onAction = onAction,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -374,7 +387,7 @@ internal fun ConfigPanel(
val audioBitRatePresetIndex = val audioBitRatePresetIndex =
presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate) presetIndexFromInput(audioBitRateKbps.toString(), ScrcpyPresets.AudioBitRate)
SectionSmallTitle(text = "Scrcpy") SectionSmallTitle("Scrcpy")
Card { Card {
SuperSwitch( SuperSwitch(
title = "音频转发", title = "音频转发",
@@ -1021,7 +1034,7 @@ internal fun DeviceTile(
else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f), else MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f),
), ),
pressFeedbackType = PressFeedbackType.Sink, pressFeedbackType = PressFeedbackType.Sink,
onClick = haptics.press, onClick = haptics.contextClick,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -1109,7 +1122,7 @@ internal fun QuickConnectCard(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(
Icons.Default.AddLink, Icons.Rounded.AddLink,
contentDescription = "快速连接", contentDescription = "快速连接",
tint = MiuixTheme.colorScheme.onPrimaryContainer, tint = MiuixTheme.colorScheme.onPrimaryContainer,
) )
@@ -1180,7 +1193,7 @@ internal fun DeviceEditorScreen(
.padding(contentPadding) .padding(contentPadding)
.padding(UiSpacing.PageHorizontal), .padding(UiSpacing.PageHorizontal),
) { ) {
SectionSmallTitle(text = "编辑设备") SectionSmallTitle("编辑设备")
Card { Card {
TextField( TextField(
value = name, value = name,

View File

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

View File

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

View File

@@ -81,7 +81,7 @@ internal fun StatusCardLayout(
.fillMaxHeight(), .fillMaxHeight(),
colors = defaultColors(color = spec.big.containerColor), colors = defaultColors(color = spec.big.containerColor),
pressFeedbackType = PressFeedbackType.Tilt, pressFeedbackType = PressFeedbackType.Tilt,
onClick = haptics.press, onClick = haptics.contextClick,
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Box( Box(
@@ -158,7 +158,7 @@ private fun StatusMetricCard(spec: StatusSmallCardSpec, modifier: Modifier) {
modifier = modifier, modifier = modifier,
insideMargin = PaddingValues(UiSpacing.Large), insideMargin = PaddingValues(UiSpacing.Large),
pressFeedbackType = PressFeedbackType.Tilt, pressFeedbackType = PressFeedbackType.Tilt,
onClick = haptics.press, onClick = haptics.contextClick,
) { ) {
Text( Text(
text = spec.title, text = spec.title,

View File

@@ -4,21 +4,23 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.VolumeDown import androidx.compose.material.icons.automirrored.rounded.VolumeDown
import androidx.compose.material.icons.automirrored.filled.VolumeOff import androidx.compose.material.icons.automirrored.rounded.VolumeOff
import androidx.compose.material.icons.automirrored.filled.VolumeUp import androidx.compose.material.icons.automirrored.rounded.VolumeUp
import androidx.compose.material.icons.filled.Apps import androidx.compose.material.icons.rounded.Apps
import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.rounded.Notifications
import androidx.compose.material.icons.filled.PhotoCamera import androidx.compose.material.icons.rounded.PowerSettingsNew
import androidx.compose.material.icons.filled.PowerSettingsNew import androidx.compose.material.icons.rounded.Screenshot
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf 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.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp 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.UiAndroidKeycodes
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing 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.Button
import top.yukonga.miuix.kmp.basic.ButtonDefaults 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.Icon
import top.yukonga.miuix.kmp.basic.ListPopupColumn import top.yukonga.miuix.kmp.basic.ListPopupColumn
import top.yukonga.miuix.kmp.basic.ListPopupDefaults import top.yukonga.miuix.kmp.basic.ListPopupDefaults
import top.yukonga.miuix.kmp.basic.PopupPositionProvider 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.basic.Text
import top.yukonga.miuix.kmp.extra.SuperListPopup import top.yukonga.miuix.kmp.extra.SuperListPopup
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme
@@ -50,13 +56,13 @@ enum class VirtualButtonAction(
MORE( MORE(
"more", "more",
"更多", "更多",
Icons.Default.MoreVert, Icons.Rounded.MoreVert,
null null
), ),
HOME( HOME(
"home", "home",
"主页", "主页",
Icons.Default.Home, Icons.Rounded.Home,
UiAndroidKeycodes.HOME UiAndroidKeycodes.HOME
), ),
BACK( BACK(
@@ -68,97 +74,87 @@ enum class VirtualButtonAction(
APP_SWITCH( APP_SWITCH(
"app_switch", "app_switch",
"多任务", "多任务",
Icons.Default.Apps, Icons.Rounded.Apps,
UiAndroidKeycodes.APP_SWITCH UiAndroidKeycodes.APP_SWITCH
), ),
MENU( MENU(
"menu", "menu",
"菜单", "菜单",
Icons.Default.Menu, Icons.Rounded.Menu,
UiAndroidKeycodes.MENU UiAndroidKeycodes.MENU
), ),
NOTIFICATION( NOTIFICATION(
"notification", "notification",
"通知栏", "通知栏",
Icons.Default.Notifications, Icons.Rounded.Notifications,
UiAndroidKeycodes.NOTIFICATION UiAndroidKeycodes.NOTIFICATION
), ),
VOLUME_UP( VOLUME_UP(
"volume_up", "volume_up",
"音量+", "音量+",
Icons.AutoMirrored.Filled.VolumeUp, Icons.AutoMirrored.Rounded.VolumeUp,
UiAndroidKeycodes.VOLUME_UP UiAndroidKeycodes.VOLUME_UP
), ),
VOLUME_DOWN( VOLUME_DOWN(
"volume_down", "volume_down",
"音量-", "音量-",
Icons.AutoMirrored.Filled.VolumeDown, Icons.AutoMirrored.Rounded.VolumeDown,
UiAndroidKeycodes.VOLUME_DOWN UiAndroidKeycodes.VOLUME_DOWN
), ),
VOLUME_MUTE( VOLUME_MUTE(
"volume_mute", "volume_mute",
"静音", "静音",
Icons.AutoMirrored.Filled.VolumeOff, Icons.AutoMirrored.Rounded.VolumeOff,
UiAndroidKeycodes.VOLUME_MUTE UiAndroidKeycodes.VOLUME_MUTE
), ),
POWER( POWER(
"power", "power",
"锁屏", "锁屏",
Icons.Default.PowerSettingsNew, Icons.Rounded.PowerSettingsNew,
UiAndroidKeycodes.POWER UiAndroidKeycodes.POWER
), ),
SCREENSHOT( SCREENSHOT(
"screenshot", "screenshot",
"截图", "截图",
Icons.Default.PhotoCamera, Icons.Rounded.Screenshot,
UiAndroidKeycodes.SYSRQ UiAndroidKeycodes.SYSRQ
), ),
} }
data class VirtualButtonItem(
val action: VirtualButtonAction,
val showOutside: Boolean,
)
object VirtualButtonActions { object VirtualButtonActions {
val all = VirtualButtonAction.entries val all = VirtualButtonAction.entries
private val byId = all.associateBy { it.id } private val byId = all.associateBy { it.id }
fun parseStoredIds(raw: String): List<String> { fun parseStoredLayout(raw: String): List<VirtualButtonItem> {
if (raw.isBlank()) return emptyList() if (raw.isBlank())
return parseStoredLayout(AppDefaults.VIRTUAL_BUTTONS_LAYOUT)
return raw.split(',').mapNotNull { item -> return raw.split(',').mapNotNull { item ->
val id = item.trim() val parts = item.trim().split(':')
id.ifBlank { null } 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>): String = ids.joinToString(",") fun encodeStoredLayout(items: List<VirtualButtonItem>): String {
return items.joinToString(",") { item ->
fun resolveLayout( "${item.action.id}:${if (item.showOutside) "1" else "0"}"
outsideIds: List<String>,
moreIds: List<String>,
): Pair<List<VirtualButtonAction>, List<VirtualButtonAction>> {
val outside = mutableListOf<VirtualButtonAction>()
val overflow = mutableListOf<VirtualButtonAction>()
val used = mutableSetOf<String>()
outsideIds.forEach { id ->
val action = byId[id] ?: return@forEach
if (used.add(action.id)) outside.add(action)
} }
}
if (used.add(VirtualButtonAction.MORE.id)) { fun splitLayout(items: List<VirtualButtonItem>): Pair<List<VirtualButtonAction>, List<VirtualButtonAction>> {
outside.add(VirtualButtonAction.MORE) val outside = items.filter { it.showOutside }.map { it.action }
} val more = items.filter { !it.showOutside }.map { it.action }
return outside to 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
} }
} }
@@ -169,11 +165,11 @@ class VirtualButtonBar(
@Composable @Composable
fun Preview( fun Preview(
enabled: Boolean, enabled: Boolean,
onPressHaptic: () -> Unit, showText: Boolean,
onConfirmHaptic: () -> Unit,
onAction: (VirtualButtonAction) -> Unit, onAction: (VirtualButtonAction) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val haptics = rememberAppHaptics()
val activeContainerColor = MiuixTheme.colorScheme.primary val activeContainerColor = MiuixTheme.colorScheme.primary
val disabledContainerColor = MiuixTheme.colorScheme.primary.copy(alpha = 0.35f) val disabledContainerColor = MiuixTheme.colorScheme.primary.copy(alpha = 0.35f)
val activeContentColor = MiuixTheme.colorScheme.onPrimary val activeContentColor = MiuixTheme.colorScheme.onPrimary
@@ -188,7 +184,7 @@ class VirtualButtonBar(
Box(modifier = Modifier.weight(1f)) { Box(modifier = Modifier.weight(1f)) {
Button( Button(
onClick = { onClick = {
onPressHaptic() haptics.contextClick()
if (action == VirtualButtonAction.MORE) { if (action == VirtualButtonAction.MORE) {
showMorePopup = true showMorePopup = true
} else { } else {
@@ -201,6 +197,7 @@ class VirtualButtonBar(
color = activeContainerColor, color = activeContainerColor,
disabledColor = disabledContainerColor, disabledColor = disabledContainerColor,
), ),
insideMargin = PaddingValues(0.dp),
) { ) {
val contentColor = if (enabled) activeContentColor else disabledContentColor val contentColor = if (enabled) activeContentColor else disabledContentColor
Icon( Icon(
@@ -209,15 +206,16 @@ class VirtualButtonBar(
modifier = Modifier.size(18.dp), modifier = Modifier.size(18.dp),
tint = contentColor, tint = contentColor,
) )
androidx.compose.foundation.layout.Spacer(Modifier.width(UiSpacing.Small)) if (showText) {
Text(action.title, color = contentColor) Spacer(Modifier.width(UiSpacing.Small))
Text(action.title, color = contentColor)
}
} }
if (action == VirtualButtonAction.MORE) { if (action == VirtualButtonAction.MORE) {
MorePopup( MorePopup(
show = showMorePopup, show = showMorePopup,
moreActions = moreActions, moreActions = moreActions,
onDismiss = { showMorePopup = false }, onDismiss = { showMorePopup = false },
onConfirmHaptic = onConfirmHaptic,
onAction = { onAction = {
onAction(it) onAction(it)
showMorePopup = false showMorePopup = false
@@ -232,11 +230,10 @@ class VirtualButtonBar(
@Composable @Composable
fun Fullscreen( fun Fullscreen(
onPressHaptic: () -> Unit,
onConfirmHaptic: () -> Unit,
onAction: (VirtualButtonAction) -> Unit, onAction: (VirtualButtonAction) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val haptics = rememberAppHaptics()
var showMorePopup by remember { mutableStateOf(false) } var showMorePopup by remember { mutableStateOf(false) }
Row( Row(
@@ -247,7 +244,7 @@ class VirtualButtonBar(
Box(modifier = Modifier.weight(1f)) { Box(modifier = Modifier.weight(1f)) {
Button( Button(
onClick = { onClick = {
onPressHaptic() haptics.contextClick()
if (action == VirtualButtonAction.MORE) { if (action == VirtualButtonAction.MORE) {
showMorePopup = true showMorePopup = true
} else { } else {
@@ -270,7 +267,6 @@ class VirtualButtonBar(
show = showMorePopup, show = showMorePopup,
moreActions = moreActions, moreActions = moreActions,
onDismiss = { showMorePopup = false }, onDismiss = { showMorePopup = false },
onConfirmHaptic = onConfirmHaptic,
onAction = { onAction = {
onAction(it) onAction(it)
showMorePopup = false showMorePopup = false
@@ -288,10 +284,26 @@ class VirtualButtonBar(
show: Boolean, show: Boolean,
moreActions: List<VirtualButtonAction>, moreActions: List<VirtualButtonAction>,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onConfirmHaptic: () -> Unit,
onAction: (VirtualButtonAction) -> Unit, onAction: (VirtualButtonAction) -> Unit,
renderInRootScaffold: Boolean, 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( SuperListPopup(
show = show, show = show,
popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,
@@ -301,15 +313,17 @@ class VirtualButtonBar(
enableWindowDim = false, enableWindowDim = false,
) { ) {
ListPopupColumn { ListPopupColumn {
moreActions.forEachIndexed { index, action -> spinnerItems.forEachIndexed { index, entry ->
DropdownImpl( SpinnerItemImpl(
text = action.title, entry = entry,
optionSize = moreActions.size, entryCount = spinnerItems.size,
isSelected = false, isSelected = false,
index = index, index = index,
onSelectedIndexChange = { spinnerColors = SpinnerDefaults.spinnerColors(),
onConfirmHaptic() dialogMode = false,
onAction(action) onSelectedIndexChange = { selectedIdx ->
haptics.confirm()
onAction(moreActions[selectedIdx])
}, },
) )
} }