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