From 98443040e645192ad61ac4ac198bb654ca27a18f Mon Sep 17 00:00:00 2001 From: Miuzarte <982809597@qq.com> Date: Fri, 10 Apr 2026 19:18:46 +0800 Subject: [PATCH] feat: adb connect following with scrcpy auto start --- .../constants/ScrcpyPresets.kt | 11 +- .../scrcpyforandroid/models/DeviceModels.kt | 19 +- .../nativecore/PersistentVideoRenderer.kt | 20 +- .../scrcpyforandroid/pages/DeviceTabScreen.kt | 155 ++++++++-------- .../pages/FullscreenControlScreen.kt | 7 +- .../scrcpyforandroid/pages/RootNavigator.kt | 1 - .../scrcpyforandroid/scrcpy/ClientOptions.kt | 3 +- .../scrcpyforandroid/services/EventLogger.kt | 1 - .../scrcpyforandroid/storage/Settings.kt | 1 - .../scrcpyforandroid/widgets/DeviceWidgets.kt | 172 +++++++++++------- .../widgets/VirtualButtons.kt | 1 - 11 files changed, 233 insertions(+), 158 deletions(-) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt index 4c4bf47..8bb5da9 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/constants/ScrcpyPresets.kt @@ -11,9 +11,9 @@ class Preset>(val values: List) { 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>(val values: List) { 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>(val values: List) { fun Preset.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 } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/models/DeviceModels.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/models/DeviceModels.kt index 6e1f9ee..fbc46c9 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/models/DeviceModels.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/models/DeviceModels.kt @@ -38,6 +38,7 @@ class DeviceShortcuts(val devices: List) : List 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) : List } 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) : List 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 } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/PersistentVideoRenderer.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/PersistentVideoRenderer.kt index f6be3e2..e1a2cc9 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/PersistentVideoRenderer.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/nativecore/PersistentVideoRenderer.kt @@ -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]}" + ) } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt index c3f3b2f..d9fa078 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/DeviceTabScreen.kt @@ -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() - 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, ) } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt index 581d7f5..6b5d267 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/FullscreenControlScreen.kt @@ -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 + ) } } diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/RootNavigator.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/RootNavigator.kt index 878287d..c074763 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/RootNavigator.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/RootNavigator.kt @@ -1,6 +1,5 @@ package io.github.miuzarte.scrcpyforandroid.pages -import android.content.pm.ActivityInfo import androidx.compose.runtime.staticCompositionLocalOf class RootNavigator( diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt index c568040..e3ae058 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/scrcpy/ClientOptions.kt @@ -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 diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/EventLogger.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/EventLogger.kt index ae02188..b0f323b 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/EventLogger.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/services/EventLogger.kt @@ -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 diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/Settings.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/Settings.kt index fd030eb..48b8291 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/Settings.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/storage/Settings.kt @@ -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 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 f2ec1a4..c511454 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 @@ -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(), ) 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 d2c3a8e..11241ef 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 @@ -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