enhance: Settings and Virtual Button Management
- 用 `reorderable` 重构排序组件 - 重构虚拟按钮配置储存逻辑 - 虚拟图标现在可以选择是否显示文字描述 - 统一所有图标风格为 `Rounded` - `StatusCard` 状态卡大卡的颜色现在优先取决于应用内设置,而非系统设置
This commit is contained in:
@@ -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/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/7a50bd1f-8095-4269-8e58-88316d86e3d8" height="320" alt="Screenshot 5" />
|
||||
</p>
|
||||
<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>
|
||||
|
||||
## 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
|
||||
|
||||
## 建议搭配模块
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"name": "miuix",
|
||||
"path": "../miuix"
|
||||
},
|
||||
{
|
||||
"name": "Reorderable",
|
||||
"path": "../Reorderable"
|
||||
},
|
||||
{
|
||||
"name": "shizuku",
|
||||
"path": "../shizuku"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
virtualButtonsInMore: List<String>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,8 +38,7 @@ data class FullscreenControlLaunch(
|
||||
fun FullscreenControlPage(
|
||||
launch: FullscreenControlLaunch,
|
||||
nativeCore: NativeCoreFacade,
|
||||
virtualButtonsOutside: List<String>,
|
||||
virtualButtonsInMore: List<String>,
|
||||
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)
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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] 里
|
||||
|
||||
@@ -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<String>,
|
||||
moreIds: List<String>,
|
||||
onLayoutChange: (outside: List<String>, more: List<String>) -> 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<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)
|
||||
}
|
||||
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()
|
||||
},
|
||||
)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<VirtualButtonAction>,
|
||||
moreActions: List<VirtualButtonAction>,
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String> {
|
||||
if (raw.isBlank()) return emptyList()
|
||||
fun parseStoredLayout(raw: String): List<VirtualButtonItem> {
|
||||
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>): String = ids.joinToString(",")
|
||||
|
||||
fun resolveLayout(
|
||||
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)
|
||||
fun encodeStoredLayout(items: List<VirtualButtonItem>): 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<VirtualButtonItem>): Pair<List<VirtualButtonAction>, List<VirtualButtonAction>> {
|
||||
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<VirtualButtonAction>,
|
||||
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])
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user