feat: adb connect following with scrcpy auto start
This commit is contained in:
@@ -28,6 +28,7 @@ class Preset<T : Comparable<T>>(val values: List<T>) {
|
||||
preset is Number && value is Number -> {
|
||||
kotlin.math.abs(preset.toDouble() - value.toDouble())
|
||||
}
|
||||
|
||||
else -> if (preset > value) 1.0 else -1.0
|
||||
}
|
||||
}?.index ?: 0
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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]}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.pages
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
|
||||
class RootNavigator(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = UiSpacing.Large),
|
||||
verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical),
|
||||
) {
|
||||
SuperTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
value = currentDraft.name,
|
||||
onValueChange = { draft = currentDraft.copy(name = it) },
|
||||
label = "设备名称",
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
SuperTextField(
|
||||
value = host,
|
||||
onValueChange = { host = it },
|
||||
value = currentDraft.host,
|
||||
onValueChange = { draft = currentDraft.copy(host = it) },
|
||||
label = "IP 地址",
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
SuperTextField(
|
||||
value = port,
|
||||
onValueChange = { port = it.filter(Char::isDigit) },
|
||||
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(),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user