fix: buffered quick connect card input, quick device ip/port modifying

This commit is contained in:
Miuzarte
2026-04-14 00:24:54 +08:00
parent 3a6df23a6c
commit 89c111e23b
7 changed files with 103 additions and 22 deletions

View File

@@ -23,6 +23,7 @@
## Features ## Features
- 带生物认证的锁屏密码自动填充 (入口位于虚拟按钮中)
- 可替换 scrcpy-server - 可替换 scrcpy-server
- 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口 - 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口
- 自动横竖屏切换(算吗 - 自动横竖屏切换(算吗

View File

@@ -40,8 +40,8 @@ android {
applicationId = "io.github.miuzarte.scrcpyforandroid" applicationId = "io.github.miuzarte.scrcpyforandroid"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 10 versionCode = 11
versionName = "0.1.4" versionName = "0.1.5"
externalNativeBuild { externalNativeBuild {
cmake { cmake {

View File

@@ -19,8 +19,24 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
separator: String = DEFAULT_SEPARATOR, separator: String = DEFAULT_SEPARATOR,
): DeviceShortcuts { ): DeviceShortcuts {
if (s.isBlank()) return DeviceShortcuts(emptyList()) if (s.isBlank()) return DeviceShortcuts(emptyList())
var nextLegacyId = 1
val list = s.splitToSequence(separator) val list = s.splitToSequence(separator)
.mapNotNull { DeviceShortcut.unmarshalFrom(it) } .mapNotNull { raw ->
val firstValue = raw.split(DeviceShortcut.DEFAULT_SEPARATOR, limit = 2)
.firstOrNull()
?.trim()
.orEmpty()
if (firstValue.toIntOrNull() != null) {
DeviceShortcut.unmarshalFrom(raw)
} else {
DeviceShortcut.unmarshalFrom(
s = raw,
fallbackId = nextLegacyId.toString(),
).also {
if (it != null) nextLegacyId++
}
}
}
.toList() .toList()
return DeviceShortcuts(list) return DeviceShortcuts(list)
} }
@@ -56,6 +72,7 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
val updateById = id != null val updateById = id != null
val updated = DeviceShortcut( val updated = DeviceShortcut(
id = old.id,
name = when { name = when {
name == null -> old.name name == null -> old.name
updateNameOnlyWhenEmpty && old.name.isNotBlank() -> old.name updateNameOnlyWhenEmpty && old.name.isNotBlank() -> old.name
@@ -88,17 +105,35 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
shortcut: DeviceShortcut, shortcut: DeviceShortcut,
index: Int? = null, index: Int? = null,
): DeviceShortcuts { ): DeviceShortcuts {
val existingIdx = getIndex(shortcut.id) val normalizedShortcut = normalizeId(shortcut)
val existingById = getIndex(normalizedShortcut.id)
val existingIdx = if (existingById >= 0) {
existingById
} else {
getIndex(normalizedShortcut.host, normalizedShortcut.port)
}
val newList = devices.toMutableList() val newList = devices.toMutableList()
if (existingIdx >= 0) { if (existingIdx >= 0) {
newList[existingIdx] = shortcut // Keep existing id stable when matching by host:port.
val existingId = devices[existingIdx].id
newList[existingIdx] = normalizedShortcut.copy(id = existingId)
} else { } else {
if (index != null) newList.add(index, shortcut) if (index != null) newList.add(index, normalizedShortcut)
else newList.add(shortcut) else newList.add(normalizedShortcut)
} }
return DeviceShortcuts(newList) return DeviceShortcuts(newList)
} }
private fun normalizeId(shortcut: DeviceShortcut): DeviceShortcut {
if (shortcut.id.toIntOrNull() != null) return shortcut
return shortcut.copy(id = nextId())
}
private fun nextId(): String {
val maxId = devices.maxOfOrNull { it.id.toIntOrNull() ?: 0 } ?: 0
return (maxId + 1).toString()
}
fun move(fromIndex: Int, toIndex: Int): DeviceShortcuts { fun move(fromIndex: Int, toIndex: Int): DeviceShortcuts {
if (fromIndex !in devices.indices || toIndex !in devices.indices) return this if (fromIndex !in devices.indices || toIndex !in devices.indices) return this
if (fromIndex == toIndex) return this if (fromIndex == toIndex) return this
@@ -121,8 +156,8 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
DeviceShortcuts(devices) DeviceShortcuts(devices)
} }
// TODO: 增加 id 字段,解决编辑端口的问题
data class DeviceShortcut( data class DeviceShortcut(
val id: String = "",
val name: String = "", val name: String = "",
val host: String, val host: String,
val port: Int = Defaults.ADB_PORT, val port: Int = Defaults.ADB_PORT,
@@ -130,11 +165,10 @@ data class DeviceShortcut(
val openFullscreenOnStart: Boolean = false, val openFullscreenOnStart: Boolean = false,
val scrcpyProfileId: String = ScrcpyOptions.GLOBAL_PROFILE_ID, val scrcpyProfileId: String = ScrcpyOptions.GLOBAL_PROFILE_ID,
) { ) {
val id: String get() = "$host:$port"
fun marshalToString( fun marshalToString(
separator: String = DEFAULT_SEPARATOR, separator: String = DEFAULT_SEPARATOR,
): String = listOf( ): String = listOf(
id.trim(),
name.trim(), name.trim(),
host.trim(), host.trim(),
port.toString(), port.toString(),
@@ -150,10 +184,45 @@ data class DeviceShortcut(
fun unmarshalFrom( fun unmarshalFrom(
s: String, s: String,
separator: String = DEFAULT_SEPARATOR, separator: String = DEFAULT_SEPARATOR,
fallbackId: String? = null,
): DeviceShortcut? { ): DeviceShortcut? {
val parts = s.split(separator) val parts = s.split(separator)
return when (parts.size) { val idInData = parts.firstOrNull()
?.trim()
?.takeIf { it.toIntOrNull() != null }
return if (idInData != null) when (parts.size) {
4, 5, 6, 7 -> {
val name = parts[1].trim()
val host = parts[2].trim()
val port = parts[3].trim().toIntOrNull() ?: Defaults.ADB_PORT
val startScrcpyOnConnect = parts.getOrNull(4)
?.trim() == "1"
val openFullscreenOnStart = startScrcpyOnConnect
&& parts.getOrNull(5)
?.trim() == "1"
val scrcpyProfileId = parts.getOrNull(6)
?.trim()
.takeUnless { it.isNullOrBlank() }
?: ScrcpyOptions.GLOBAL_PROFILE_ID
if (host.isNotBlank()) DeviceShortcut(
id = idInData,
name = name,
host = host,
port = port,
startScrcpyOnConnect = startScrcpyOnConnect,
openFullscreenOnStart = openFullscreenOnStart,
scrcpyProfileId = scrcpyProfileId,
)
else null
}
else -> null
}
else when (parts.size) {
3, 4, 5, 6 -> { 3, 4, 5, 6 -> {
val id = fallbackId ?: return null
val name = parts[0].trim() val name = parts[0].trim()
val host = parts[1].trim() val host = parts[1].trim()
val port = parts[2].trim().toIntOrNull() ?: Defaults.ADB_PORT val port = parts[2].trim().toIntOrNull() ?: Defaults.ADB_PORT
@@ -169,6 +238,7 @@ data class DeviceShortcut(
?: ScrcpyOptions.GLOBAL_PROFILE_ID ?: ScrcpyOptions.GLOBAL_PROFILE_ID
if (host.isNotBlank()) DeviceShortcut( if (host.isNotBlank()) DeviceShortcut(
id = id,
name = name, name = name,
host = host, host = host,
port = port, port = port,

View File

@@ -185,7 +185,7 @@ fun DeviceTabPage(
scrcpy: Scrcpy, scrcpy: Scrcpy,
) { ) {
val activity = LocalActivity.current val activity = LocalActivity.current
val fragmentActivity = remember(activity) {activity as? FragmentActivity} val fragmentActivity = remember(activity) { activity as? FragmentActivity }
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -320,10 +320,14 @@ fun DeviceTabPage(
VirtualButtonActions.parseStoredLayout(asBundle.virtualButtonsLayout) VirtualButtonActions.parseStoredLayout(asBundle.virtualButtonsLayout)
) )
} }
var quickConnectInputTemp by rememberSaveable(qdBundle.quickConnectInput) {
mutableStateOf(qdBundle.quickConnectInput)
}
var savedShortcuts by remember { var savedShortcuts by remember {
mutableStateOf(DeviceShortcuts.unmarshalFrom(qdBundle.quickDevicesList)) mutableStateOf(DeviceShortcuts.unmarshalFrom(qdBundle.quickDevicesList))
} }
LaunchedEffect(qdBundle.quickDevicesList) { LaunchedEffect(qdBundle.quickDevicesList) {
savedShortcuts = DeviceShortcuts.unmarshalFrom(qdBundle.quickDevicesList) savedShortcuts = DeviceShortcuts.unmarshalFrom(qdBundle.quickDevicesList)
} }
@@ -1008,17 +1012,24 @@ fun DeviceTabPage(
// "快速连接" // "快速连接"
item { item {
QuickConnectCard( QuickConnectCard(
input = qdBundle.quickConnectInput, input = quickConnectInputTemp,
onValueChange = { onValueChange = {
qdBundle = qdBundle.copy(quickConnectInput = it) quickConnectInputTemp = it
},
onFocusLost = {
qdBundle = qdBundle.copy(
quickConnectInput = quickConnectInputTemp
)
}, },
enabled = !adbConnecting, enabled = !adbConnecting,
onAddDevice = { onAddDevice = {
val target = ConnectionTarget.unmarshalFrom(qdBundle.quickConnectInput) val target = ConnectionTarget.unmarshalFrom(quickConnectInputTemp)
?: return@QuickConnectCard ?: return@QuickConnectCard
savedShortcuts = savedShortcuts.upsert( savedShortcuts = savedShortcuts.upsert(
DeviceShortcut(host = target.host, port = target.port) DeviceShortcut(host = target.host, port = target.port)
) )
Log.d( Log.d(
"SavedShortcuts", "SavedShortcuts",
"size: ${savedShortcuts.size}, list: ${qdBundle.quickDevicesList}" "size: ${savedShortcuts.size}, list: ${qdBundle.quickDevicesList}"
@@ -1026,8 +1037,9 @@ fun DeviceTabPage(
snackbar.show("已添加设备: ${target.host}:${target.port}") snackbar.show("已添加设备: ${target.host}:${target.port}")
}, },
onConnect = { onConnect = {
val target = ConnectionTarget.unmarshalFrom(qdBundle.quickConnectInput) val target = ConnectionTarget.unmarshalFrom(quickConnectInputTemp)
?: return@QuickConnectCard ?: return@QuickConnectCard
adbCallbacks.onQuickConnect(target) adbCallbacks.onQuickConnect(target)
}, },
) )

View File

@@ -492,7 +492,6 @@ fun SettingsPage(
item { item {
SectionSmallTitle("") SectionSmallTitle("")
Card { Card {
// TODO: 进入时无视自动更新检查的 CD, 主动触发一次
ArrowPreference( ArrowPreference(
title = "关于", title = "关于",
summary = updateSummary, summary = updateSummary,

View File

@@ -11,8 +11,6 @@ import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.OrientationLock
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Tick import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.Tick
import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource import io.github.miuzarte.scrcpyforandroid.scrcpy.Shared.VideoSource
// TODO: 配置切换
data class ClientOptions( data class ClientOptions(
// var serial: String = "", // to server // var serial: String = "", // to server

View File

@@ -953,6 +953,7 @@ internal fun DeviceTile(
val trimmedHost = currentDraft.host.trim() val trimmedHost = currentDraft.host.trim()
if (trimmedHost.isBlank()) return@LaunchedEffect if (trimmedHost.isBlank()) return@LaunchedEffect
val updated = DeviceShortcut( val updated = DeviceShortcut(
id = currentDraft.id,
name = currentDraft.name.trim(), name = currentDraft.name.trim(),
host = trimmedHost, host = trimmedHost,
port = currentDraft.port, port = currentDraft.port,
@@ -1200,7 +1201,7 @@ internal fun DeviceTileList(
internal fun QuickConnectCard( internal fun QuickConnectCard(
input: String, input: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
onFocusChange: (() -> Unit)? = null, onFocusLost: (() -> Unit)? = null,
onConnect: () -> Unit, onConnect: () -> Unit,
onAddDevice: () -> Unit, onAddDevice: () -> Unit,
enabled: Boolean = true, enabled: Boolean = true,
@@ -1240,7 +1241,7 @@ internal fun QuickConnectCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
onFocusLost = onFocusChange, onFocusLost = onFocusLost,
) )
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),