feat: adb connect following with scrcpy auto start

This commit is contained in:
Miuzarte
2026-04-10 19:18:46 +08:00
parent e734891eb3
commit 98443040e6
11 changed files with 233 additions and 158 deletions

View File

@@ -11,9 +11,9 @@ class Preset<T : Comparable<T>>(val values: List<T>) {
val lastIndex: Int get() = values.lastIndex
val size: Int get() = values.size
val indices: IntRange get() = values.indices
operator fun get(index: Int): T = values[index]
/**
* Find the index of the exact value or the nearest preset.
* For numeric types, finds the closest value by absolute difference.
@@ -21,13 +21,14 @@ class Preset<T : Comparable<T>>(val values: List<T>) {
fun indexOfOrNearest(value: T): Int {
val exact = values.indexOf(value)
if (exact >= 0) return exact
// For numeric types, find nearest by comparing
return values.withIndex().minByOrNull { (_, preset) ->
when {
preset is Number && value is Number -> {
kotlin.math.abs(preset.toDouble() - value.toDouble())
}
else -> if (preset > value) 1.0 else -1.0
}
}?.index ?: 0
@@ -40,8 +41,8 @@ class Preset<T : Comparable<T>>(val values: List<T>) {
fun Preset<Int>.indexOfOrNearest(raw: Int): Int {
val exact = values.indexOf(raw)
if (exact >= 0) return exact
val nearest = values.withIndex().minByOrNull { (_, preset) ->
kotlin.math.abs(preset - raw)
val nearest = values.withIndex().minByOrNull { (_, preset) ->
kotlin.math.abs(preset - raw)
}
return nearest?.index ?: 0
}

View File

@@ -38,6 +38,7 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
host: String? = null,
port: Int? = null,
name: String? = null,
startScrcpyOnConnect: Boolean? = null,
newPort: Int? = null,
updateNameOnlyWhenEmpty: Boolean = false,
): DeviceShortcuts {
@@ -57,9 +58,15 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
}
val finalHost = if (updateById) host ?: old.host else old.host
val finalPort = if (updateById) port ?: old.port else newPort ?: old.port
val finalStartScrcpyOnConnect = startScrcpyOnConnect ?: old.startScrcpyOnConnect
// 若无任何变化,返回原实例
if (finalName == old.name && finalHost == old.host && finalPort == old.port)
if (
finalName == old.name
&& finalHost == old.host
&& finalPort == old.port
&& finalStartScrcpyOnConnect == old.startScrcpyOnConnect
)
return this
val newList = devices.toMutableList().apply {
@@ -67,6 +74,7 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
name = finalName,
host = finalHost,
port = finalPort,
startScrcpyOnConnect = finalStartScrcpyOnConnect,
)
}
return DeviceShortcuts(
@@ -119,13 +127,14 @@ data class DeviceShortcut(
val name: String = "",
val host: String,
val port: Int = Defaults.ADB_PORT,
val startScrcpyOnConnect: Boolean = false,
) {
val id: String get() = "$host:$port"
fun marshalToString(
separator: String = DEFAULT_SEPARATOR,
): String = listOf(
name.trim(), host.trim(), port.toString()
name.trim(), host.trim(), port.toString(), if (startScrcpyOnConnect) "1" else "0"
).joinToString(
separator = separator
)
@@ -136,16 +145,18 @@ data class DeviceShortcut(
s: String,
separator: String = DEFAULT_SEPARATOR,
): DeviceShortcut? {
val parts = s.split(separator, limit = 3)
val parts = s.split(separator)
return when (parts.size) {
3 -> {
3, 4 -> {
val name = parts[0].trim()
val host = parts[1].trim()
val port = parts[2].trim().toIntOrNull() ?: Defaults.ADB_PORT
val startScrcpyOnConnect = parts.getOrNull(3)?.trim() == "1"
if (host.isNotBlank()) DeviceShortcut(
name = name,
host = host,
port = port,
startScrcpyOnConnect = startScrcpyOnConnect,
)
else null
}

View File

@@ -87,7 +87,10 @@ class PersistentVideoRenderer {
fun detachDisplaySurface(surface: Surface? = null, releaseSurface: Boolean = false) {
val requestId = surface?.let { System.identityHashCode(it) }
Log.i(tag, "detachDisplaySurface(): request surfaceId=$requestId releaseSurface=$releaseSurface current=${displaySurfaceId}")
Log.i(
tag,
"detachDisplaySurface(): request surfaceId=$requestId releaseSurface=$releaseSurface current=${displaySurfaceId}"
)
handler.post {
if (released) return@post
if (requestId != null && requestId != displaySurfaceId) return@post
@@ -190,7 +193,10 @@ class PersistentVideoRenderer {
setOnFrameAvailableListener({
val n = frameAvailableCount.incrementAndGet()
if (n == 1L || n % 120L == 0L) {
Log.i(tag, "onFrameAvailable(): available=$n consumed=${frameConsumedCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}")
Log.i(
tag,
"onFrameAvailable(): available=$n consumed=${frameConsumedCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}"
)
}
drawFrame()
}, handler)
@@ -222,7 +228,10 @@ class PersistentVideoRenderer {
.onSuccess {
val consumed = frameConsumedCount.incrementAndGet()
if (consumed == 1L || consumed % 120L == 0L) {
Log.i(tag, "drawFrame(): consumed=$consumed available=${frameAvailableCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}")
Log.i(
tag,
"drawFrame(): consumed=$consumed available=${frameAvailableCount.get()} rendered=${frameRenderedCount.get()} display=${displaySurfaceId != null}"
)
}
}
.onFailure { Log.w(tag, "updateTexImage failed", it) }
@@ -258,7 +267,10 @@ class PersistentVideoRenderer {
EGL14.eglSwapBuffers(eglDisplay, displayEglSurface)
val rendered = frameRenderedCount.incrementAndGet()
if (rendered == 1L || rendered % 120L == 0L) {
Log.i(tag, "drawFrame(): rendered=$rendered consumed=${frameConsumedCount.get()} available=${frameAvailableCount.get()} viewport=${width[0]}x${height[0]}")
Log.i(
tag,
"drawFrame(): rendered=$rendered consumed=${frameConsumedCount.get()} available=${frameAvailableCount.get()} viewport=${width[0]}x${height[0]}"
)
}
}

View File

@@ -503,6 +503,63 @@ fun DeviceTabPage(
}
}
LaunchedEffect(adbConnected, currentTargetHost, currentTargetPort) {
if (!adbConnected || currentTargetHost.isBlank()) return@LaunchedEffect
// Keep-alive loop for current target.
// On failure: try to reconnect once; if failed, fully disconnect and reset UI state.
val host = currentTargetHost
val port = currentTargetPort
while (adbConnected && currentTargetHost == host && currentTargetPort == port) {
delay(ADB_KEEPALIVE_INTERVAL_MS)
val alive = runCatching { keepAliveCheck(host, port) }.getOrElse { false }
if (alive) continue
logEvent("ADB 长连接中断,尝试自动重连: $host:$port", Log.WARN)
try {
connectWithTimeout(host, port)
adbConnected = true
statusLine = "$host:$port"
logEvent("ADB 自动重连成功: $host:$port")
snackbar.show("ADB 自动重连成功")
} catch (e: Exception) {
disconnectAdbConnection()
statusLine = "ADB 连接断开"
logEvent("ADB 自动重连失败: $e", Log.ERROR)
snackbar.show("ADB 自动重连失败")
break
}
}
}
suspend fun startScrcpySession() {
val options = scrcpyOptions.toClientOptions(soBundleShared).fix()
val session = scrcpy.start(options)
if (options.disableScreensaver) {
setKeepScreenOn(true)
}
statusLine = "scrcpy 运行中"
@SuppressLint("DefaultLocale")
val videoDetail =
if (!options.video) "off"
else if (soBundleShared.videoBitRate <= 0) "${session.codec?.string ?: "null"} ${session.width}x${session.height} @default"
else "${session.codec?.string ?: "null"} ${session.width}x${session.height} " +
"@${String.format("%.1f", soBundleShared.videoBitRate / 1_000_000f)}Mbps"
val audioDetail =
if (!soBundleShared.audio) "off"
else if (soBundleShared.audioBitRate <= 0) "${options.audioCodec} default source=${options.audioSource}"
else "${options.audioCodec} ${soBundleShared.audioBitRate / 1_000f}Kbps source=${options.audioSource}${if (!options.audioPlayback) "(no-playback)" else ""}"
logEvent(
"scrcpy 已启动: device=${session.deviceName}" +
", video=$videoDetail, audio=$audioDetail" +
", control=${options.control}, turnScreenOff=${options.turnScreenOff}" +
", maxSize=${options.maxSize}, maxFps=${options.maxFps}"
)
snackbar.show("scrcpy 已启动")
}
suspend fun handleAdbConnected(host: String, port: Int) {
currentTargetHost = host
currentTargetPort = port
@@ -534,48 +591,28 @@ fun DeviceTabPage(
"sdk=${info.sdkInt}"
)
snackbar.show("ADB 已连接")
}
LaunchedEffect(adbConnected, currentTargetHost, currentTargetPort) {
if (!adbConnected || currentTargetHost.isBlank()) return@LaunchedEffect
// Keep-alive loop for current target.
// On failure: try to reconnect once; if failed, fully disconnect and reset UI state.
val host = currentTargetHost
val port = currentTargetPort
while (adbConnected && currentTargetHost == host && currentTargetPort == port) {
delay(ADB_KEEPALIVE_INTERVAL_MS)
val alive = runCatching { keepAliveCheck(host, port) }.getOrElse { false }
if (alive) continue
logEvent("ADB 长连接中断,尝试自动重连: $host:$port", Log.WARN)
try {
connectWithTimeout(host, port)
adbConnected = true
statusLine = "$host:$port"
logEvent("ADB 自动重连成功: $host:$port")
snackbar.show("ADB 自动重连成功")
} catch (e: Exception) {
disconnectAdbConnection()
statusLine = "ADB 连接断开"
logEvent("ADB 自动重连失败: $e", Log.ERROR)
snackbar.show("ADB 自动重连失败")
break
if (
savedShortcuts.get(host, port)?.startScrcpyOnConnect == true &&
sessionInfo == null
) {
runBusy("启动 scrcpy") {
startScrcpySession()
}
}
}
val adbPairingAutoDiscoverOnDialogOpen = asBundle.adbPairingAutoDiscoverOnDialogOpen
val adbAutoReconnectPairedDevice = asBundle.adbAutoReconnectPairedDevice
val adbMdnsLanDiscovery = asBundle.adbMdnsLanDiscovery
LaunchedEffect(adbConnected, adbAutoReconnectPairedDevice, adbMdnsLanDiscovery) {
if (adbConnected || !adbAutoReconnectPairedDevice) return@LaunchedEffect
LaunchedEffect(
adbConnected,
asBundle.adbAutoReconnectPairedDevice,
asBundle.adbMdnsLanDiscovery
) {
if (adbConnected || !asBundle.adbAutoReconnectPairedDevice) return@LaunchedEffect
// Background auto reconnect pipeline:
// 1) try quick list targets with reachable TCP ports
// 2) fallback to mDNS discovery
val quickConnectTriedOnce = mutableSetOf<String>()
while (!adbConnected && adbAutoReconnectPairedDevice) {
while (!adbConnected) {
if (busy || adbConnecting || sessionInfo != null) {
delay(ADB_AUTO_RECONNECT_RETRY_INTERVAL_MS)
continue
@@ -611,7 +648,7 @@ fun DeviceTabPage(
val discovered = withContext(Dispatchers.IO) {
adbService.discoverConnectService(
timeoutMs = ADB_AUTO_RECONNECT_DISCOVER_TIMEOUT_MS,
includeLanDevices = adbMdnsLanDiscovery,
includeLanDevices = asBundle.adbMdnsLanDiscovery,
)
}
@@ -673,12 +710,6 @@ fun DeviceTabPage(
}
}
val devicePreviewCardHeightDp = asBundle.devicePreviewCardHeightDp
val previewVirtualButtonShowText = asBundle.previewVirtualButtonShowText
val audioBitRate = soBundleShared.audioBitRate
val videoBitRate = soBundleShared.videoBitRate
// 设备
LazyColumn(
contentPadding = contentPadding,
@@ -708,7 +739,9 @@ fun DeviceTabPage(
adbConnecting && activeDeviceActionId == device.id
},
editingDeviceId = editingDeviceId,
onClick = {},
onClick = {
snackbar.show("长按可编辑")
},
onLongClick = { device ->
val connected = adbConnected
&& currentTarget?.host == device.host
@@ -716,7 +749,9 @@ fun DeviceTabPage(
if (connected) {
snackbar.show("无法修改已连接的设备")
} else {
editingDeviceId = device.id
editingDeviceId =
if (editingDeviceId != device.id) device.id
else null
}
},
onAction = { device ->
@@ -769,8 +804,8 @@ fun DeviceTabPage(
name = updated.name,
host = updated.host,
port = updated.port,
startScrcpyOnConnect = updated.startScrcpyOnConnect,
)
editingDeviceId = null
},
onEditorDelete = { device ->
savedShortcuts = savedShortcuts.remove(id = device.id)
@@ -833,10 +868,10 @@ fun DeviceTabPage(
// "使用配对码配对设备"
PairingCard(
busy = busy,
autoDiscoverOnDialogOpen = adbPairingAutoDiscoverOnDialogOpen,
autoDiscoverOnDialogOpen = asBundle.adbPairingAutoDiscoverOnDialogOpen,
onDiscoverTarget = {
adbService.discoverPairingService(
includeLanDevices = adbMdnsLanDiscovery,
includeLanDevices = asBundle.adbMdnsLanDiscovery,
)
},
onPair = { host, port, code ->
@@ -873,31 +908,7 @@ fun DeviceTabPage(
onStartStopHaptic = { haptics.contextClick() },
onStart = {
runBusy("启动 scrcpy") {
val options = scrcpyOptions.toClientOptions(soBundleShared).fix()
val session = scrcpy.start(options)
if (options.disableScreensaver) {
setKeepScreenOn(true)
}
statusLine = "scrcpy 运行中"
@SuppressLint("DefaultLocale")
val videoDetail =
if (!options.video) "off"
else if (videoBitRate <= 0) "${session.codec?.string ?: "null"} ${session.width}x${session.height} @default"
else "${session.codec?.string ?: "null"} ${session.width}x${session.height} " +
"@${String.format("%.1f", videoBitRate / 1_000_000f)}Mbps"
val audioDetail =
if (!soBundleShared.audio) "off"
else if (audioBitRate <= 0) "${options.audioCodec} default source=${options.audioSource}"
else "${options.audioCodec} ${audioBitRate / 1_000f}Kbps source=${options.audioSource}${if (!options.audioPlayback) "(no-playback)" else ""}"
logEvent(
"scrcpy 已启动: device=${session.deviceName}" +
", video=$videoDetail, audio=$audioDetail" +
", control=${options.control}, turnScreenOff=${options.turnScreenOff}" +
", maxSize=${options.maxSize}, maxFps=${options.maxFps}"
)
snackbar.show("scrcpy 已启动")
startScrcpySession()
}
},
onStop = {
@@ -938,7 +949,7 @@ fun DeviceTabPage(
PreviewCard(
sessionInfo = sessionInfo,
nativeCore = nativeCore,
previewHeightDp = devicePreviewCardHeightDp.coerceAtLeast(120),
previewHeightDp = asBundle.devicePreviewCardHeightDp.coerceAtLeast(120),
controlsVisible = previewControlsVisible,
onTapped = {
previewControlsVisible = !previewControlsVisible
@@ -964,7 +975,7 @@ fun DeviceTabPage(
busy = busy,
outsideActions = virtualButtonLayout.first,
moreActions = virtualButtonLayout.second,
showText = previewVirtualButtonShowText,
showText = asBundle.previewVirtualButtonShowText,
onAction = ::sendVirtualButtonAction,
)
}

View File

@@ -1,6 +1,7 @@
package io.github.miuzarte.scrcpyforandroid.pages
import android.app.Activity
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
@@ -144,7 +145,11 @@ fun FullscreenControlScreen(
nativeCore.session?.injectKeycode(1, keycode)
}
}.onFailure { e ->
android.util.Log.w("FullscreenControlPage", "sendKeycode failed for keycode=$keycode", e)
Log.w(
"FullscreenControlPage",
"sendKeycode failed for keycode=$keycode",
e
)
}
}

View File

@@ -1,6 +1,5 @@
package io.github.miuzarte.scrcpyforandroid.pages
import android.content.pm.ActivityInfo
import androidx.compose.runtime.staticCompositionLocalOf
class RootNavigator(

View File

@@ -229,7 +229,7 @@ data class ClientOptions(
}
fun fix(): ClientOptions {
when(videoSource) {
when (videoSource) {
VideoSource.DISPLAY -> {
cameraId = ""
cameraFacing = CameraFacing.ANY
@@ -238,6 +238,7 @@ data class ClientOptions(
cameraFps = 0u
cameraHighSpeed = false
}
VideoSource.CAMERA -> {
displayId = 0
maxSize = 0u

View File

@@ -3,7 +3,6 @@ package io.github.miuzarte.scrcpyforandroid.services
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import io.github.miuzarte.scrcpyforandroid.constants.Defaults
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

View File

@@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.datastore.core.DataStore

View File

@@ -97,6 +97,8 @@ import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.overlay.OverlayDialog
import top.yukonga.miuix.kmp.preference.ArrowPreference
import top.yukonga.miuix.kmp.preference.CheckboxLocation
import top.yukonga.miuix.kmp.preference.CheckboxPreference
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
import top.yukonga.miuix.kmp.preference.SwitchPreference
import top.yukonga.miuix.kmp.theme.MiuixTheme
@@ -435,23 +437,23 @@ internal fun ConfigPanel(
enabled = !sessionStarted && soBundle.audio,
)
AnimatedVisibility(audioBitRateVisibility) {
SuperSlider(
title = "音频码率",
summary = "--audio-bit-rate",
value = ScrcpyPresets.AudioBitRate
.indexOfOrNearest(soBundle.audioBitRate / 1000)
.toFloat(),
onValueChange = { value ->
val idx = value.roundToInt()
.coerceIn(0, ScrcpyPresets.AudioBitRate.lastIndex)
soBundle = soBundle.copy(
audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000
)
},
valueRange = 0f..ScrcpyPresets.AudioBitRate.lastIndex.toFloat(),
steps = (ScrcpyPresets.AudioBitRate.size - 2).coerceAtLeast(0),
unit = "Kbps",
zeroStateText = "默认",
SuperSlider(
title = "音频码率",
summary = "--audio-bit-rate",
value = ScrcpyPresets.AudioBitRate
.indexOfOrNearest(soBundle.audioBitRate / 1000)
.toFloat(),
onValueChange = { value ->
val idx = value.roundToInt()
.coerceIn(0, ScrcpyPresets.AudioBitRate.lastIndex)
soBundle = soBundle.copy(
audioBitRate = ScrcpyPresets.AudioBitRate[idx] * 1000
)
},
valueRange = 0f..ScrcpyPresets.AudioBitRate.lastIndex.toFloat(),
steps = (ScrcpyPresets.AudioBitRate.size - 2).coerceAtLeast(0),
unit = "Kbps",
zeroStateText = "默认",
displayText = (soBundle.audioBitRate / 1_000).toString(),
inputInitialValue =
if (soBundle.audioBitRate <= 0) ""
@@ -963,15 +965,37 @@ internal fun DeviceTile(
onEditorCancel: () -> Unit,
) {
val haptics = rememberAppHaptics()
var name by rememberSaveable(device.id) { mutableStateOf(device.name) }
var host by rememberSaveable(device.id) { mutableStateOf(device.host) }
var port by rememberSaveable(device.id) { mutableStateOf(device.port.toString()) }
LaunchedEffect(device) {
name = device.name
host = device.host
port = device.port.toString()
var draft by remember(editing, device.id) {
mutableStateOf(if (editing) device else null)
}
var originalDraft by remember(editing, device.id) {
mutableStateOf(if (editing) device else null)
}
var draftPortText by remember(editing, device.id) {
mutableStateOf(if (editing) device.port.toString() else null)
}
LaunchedEffect(editing, draft) {
val currentDraft = draft ?: return@LaunchedEffect
if (!editing) return@LaunchedEffect
delay(Settings.BUNDLE_SAVE_DELAY)
val trimmedHost = currentDraft.host.trim()
if (trimmedHost.isBlank()) return@LaunchedEffect
val updated = DeviceShortcut(
name = currentDraft.name.trim(),
host = trimmedHost,
port = currentDraft.port,
startScrcpyOnConnect = currentDraft.startScrcpyOnConnect,
)
if (updated != device) {
onEditorSave(updated)
}
}
val currentDraft = draft ?: device
val currentOriginalDraft = originalDraft ?: device
val currentDraftPortText = draftPortText ?: device.port.toString()
Card(
colors = CardDefaults.defaultColors(
@@ -1041,48 +1065,73 @@ internal fun DeviceTile(
text = if (!isConnected) "连接" else "断开",
onClick = onAction,
enabled = actionEnabled && !actionInProgress,
colors = if (!isConnected && device.startScrcpyOnConnect) {
ButtonDefaults.textButtonColorsPrimary()
} else {
ButtonDefaults.textButtonColors()
},
)
}
}
AnimatedVisibility(editing) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = UiSpacing.ContentVertical)
.padding(bottom = UiSpacing.ContentVertical),
verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical)
modifier = Modifier.padding(vertical = UiSpacing.Large),
verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical),
) {
SuperTextField(
value = name,
onValueChange = { name = it },
label = "设备名称",
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
SuperTextField(
value = host,
onValueChange = { host = it },
label = "IP 地址",
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
)
SuperTextField(
value = port,
onValueChange = { port = it.filter(Char::isDigit) },
label = "端口",
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
)
Column(
modifier = Modifier.padding(horizontal = UiSpacing.Large),
verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical),
) {
SuperTextField(
value = currentDraft.name,
onValueChange = { draft = currentDraft.copy(name = it) },
label = "设备名称",
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
SuperTextField(
value = currentDraft.host,
onValueChange = { draft = currentDraft.copy(host = it) },
label = "IP 地址",
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
)
SuperTextField(
value = currentDraftPortText,
onValueChange = {
draftPortText = it.filter(Char::isDigit)
draft = currentDraft.copy(
port = draftPortText?.toIntOrNull() ?: Defaults.ADB_PORT
)
},
label = "端口",
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
)
CheckboxPreference(
title = "连接后立即启动 scrcpy",
checkboxLocation = CheckboxLocation.End,
checked = currentDraft.startScrcpyOnConnect,
onCheckedChange = {
draft = currentDraft.copy(startScrcpyOnConnect = it)
},
)
}
Row(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = UiSpacing.Large),
horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical),
) {
TextButton(
text = "取消",
onClick = onEditorCancel,
onClick = {
onEditorSave(currentOriginalDraft)
onEditorCancel()
},
modifier = Modifier.weight(1f),
)
TextButton(
@@ -1091,19 +1140,8 @@ internal fun DeviceTile(
modifier = Modifier.weight(1f),
)
TextButton(
text = "保存",
onClick = {
val trimmedHost = host.trim()
if (trimmedHost.isNotBlank()) {
onEditorSave(
DeviceShortcut(
name = name.trim(),
host = trimmedHost,
port = port.toIntOrNull() ?: Defaults.ADB_PORT,
),
)
}
},
text = "完成",
onClick = onEditorCancel,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary(),
)

View File

@@ -1,6 +1,5 @@
package io.github.miuzarte.scrcpyforandroid.widgets
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement