fix: buffered quick connect card input, quick device ip/port modifying
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
|
||||
## Features
|
||||
|
||||
- 带生物认证的锁屏密码自动填充 (入口位于虚拟按钮中)
|
||||
- 可替换 scrcpy-server
|
||||
- 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口
|
||||
- 自动横竖屏切换(算吗
|
||||
|
||||
@@ -40,8 +40,8 @@ android {
|
||||
applicationId = "io.github.miuzarte.scrcpyforandroid"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 10
|
||||
versionName = "0.1.4"
|
||||
versionCode = 11
|
||||
versionName = "0.1.5"
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
|
||||
@@ -19,8 +19,24 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|
||||
separator: String = DEFAULT_SEPARATOR,
|
||||
): DeviceShortcuts {
|
||||
if (s.isBlank()) return DeviceShortcuts(emptyList())
|
||||
var nextLegacyId = 1
|
||||
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()
|
||||
return DeviceShortcuts(list)
|
||||
}
|
||||
@@ -56,6 +72,7 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|
||||
val updateById = id != null
|
||||
|
||||
val updated = DeviceShortcut(
|
||||
id = old.id,
|
||||
name = when {
|
||||
name == null -> old.name
|
||||
updateNameOnlyWhenEmpty && old.name.isNotBlank() -> old.name
|
||||
@@ -88,17 +105,35 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|
||||
shortcut: DeviceShortcut,
|
||||
index: Int? = null,
|
||||
): 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()
|
||||
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 {
|
||||
if (index != null) newList.add(index, shortcut)
|
||||
else newList.add(shortcut)
|
||||
if (index != null) newList.add(index, normalizedShortcut)
|
||||
else newList.add(normalizedShortcut)
|
||||
}
|
||||
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 {
|
||||
if (fromIndex !in devices.indices || toIndex !in devices.indices) return this
|
||||
if (fromIndex == toIndex) return this
|
||||
@@ -121,8 +156,8 @@ class DeviceShortcuts(val devices: List<DeviceShortcut>) : List<DeviceShortcut>
|
||||
DeviceShortcuts(devices)
|
||||
}
|
||||
|
||||
// TODO: 增加 id 字段,解决编辑端口的问题
|
||||
data class DeviceShortcut(
|
||||
val id: String = "",
|
||||
val name: String = "",
|
||||
val host: String,
|
||||
val port: Int = Defaults.ADB_PORT,
|
||||
@@ -130,11 +165,10 @@ data class DeviceShortcut(
|
||||
val openFullscreenOnStart: Boolean = false,
|
||||
val scrcpyProfileId: String = ScrcpyOptions.GLOBAL_PROFILE_ID,
|
||||
) {
|
||||
val id: String get() = "$host:$port"
|
||||
|
||||
fun marshalToString(
|
||||
separator: String = DEFAULT_SEPARATOR,
|
||||
): String = listOf(
|
||||
id.trim(),
|
||||
name.trim(),
|
||||
host.trim(),
|
||||
port.toString(),
|
||||
@@ -150,10 +184,45 @@ data class DeviceShortcut(
|
||||
fun unmarshalFrom(
|
||||
s: String,
|
||||
separator: String = DEFAULT_SEPARATOR,
|
||||
fallbackId: String? = null,
|
||||
): DeviceShortcut? {
|
||||
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 -> {
|
||||
val id = fallbackId ?: return null
|
||||
val name = parts[0].trim()
|
||||
val host = parts[1].trim()
|
||||
val port = parts[2].trim().toIntOrNull() ?: Defaults.ADB_PORT
|
||||
@@ -169,6 +238,7 @@ data class DeviceShortcut(
|
||||
?: ScrcpyOptions.GLOBAL_PROFILE_ID
|
||||
|
||||
if (host.isNotBlank()) DeviceShortcut(
|
||||
id = id,
|
||||
name = name,
|
||||
host = host,
|
||||
port = port,
|
||||
|
||||
@@ -320,10 +320,14 @@ fun DeviceTabPage(
|
||||
VirtualButtonActions.parseStoredLayout(asBundle.virtualButtonsLayout)
|
||||
)
|
||||
}
|
||||
|
||||
var quickConnectInputTemp by rememberSaveable(qdBundle.quickConnectInput) {
|
||||
mutableStateOf(qdBundle.quickConnectInput)
|
||||
}
|
||||
|
||||
var savedShortcuts by remember {
|
||||
mutableStateOf(DeviceShortcuts.unmarshalFrom(qdBundle.quickDevicesList))
|
||||
}
|
||||
|
||||
LaunchedEffect(qdBundle.quickDevicesList) {
|
||||
savedShortcuts = DeviceShortcuts.unmarshalFrom(qdBundle.quickDevicesList)
|
||||
}
|
||||
@@ -1008,17 +1012,24 @@ fun DeviceTabPage(
|
||||
// "快速连接"
|
||||
item {
|
||||
QuickConnectCard(
|
||||
input = qdBundle.quickConnectInput,
|
||||
input = quickConnectInputTemp,
|
||||
onValueChange = {
|
||||
qdBundle = qdBundle.copy(quickConnectInput = it)
|
||||
quickConnectInputTemp = it
|
||||
},
|
||||
onFocusLost = {
|
||||
qdBundle = qdBundle.copy(
|
||||
quickConnectInput = quickConnectInputTemp
|
||||
)
|
||||
},
|
||||
enabled = !adbConnecting,
|
||||
onAddDevice = {
|
||||
val target = ConnectionTarget.unmarshalFrom(qdBundle.quickConnectInput)
|
||||
val target = ConnectionTarget.unmarshalFrom(quickConnectInputTemp)
|
||||
?: return@QuickConnectCard
|
||||
|
||||
savedShortcuts = savedShortcuts.upsert(
|
||||
DeviceShortcut(host = target.host, port = target.port)
|
||||
)
|
||||
|
||||
Log.d(
|
||||
"SavedShortcuts",
|
||||
"size: ${savedShortcuts.size}, list: ${qdBundle.quickDevicesList}"
|
||||
@@ -1026,8 +1037,9 @@ fun DeviceTabPage(
|
||||
snackbar.show("已添加设备: ${target.host}:${target.port}")
|
||||
},
|
||||
onConnect = {
|
||||
val target = ConnectionTarget.unmarshalFrom(qdBundle.quickConnectInput)
|
||||
val target = ConnectionTarget.unmarshalFrom(quickConnectInputTemp)
|
||||
?: return@QuickConnectCard
|
||||
|
||||
adbCallbacks.onQuickConnect(target)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -492,7 +492,6 @@ fun SettingsPage(
|
||||
item {
|
||||
SectionSmallTitle("")
|
||||
Card {
|
||||
// TODO: 进入时无视自动更新检查的 CD, 主动触发一次
|
||||
ArrowPreference(
|
||||
title = "关于",
|
||||
summary = updateSummary,
|
||||
|
||||
@@ -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.VideoSource
|
||||
|
||||
// TODO: 配置切换
|
||||
|
||||
data class ClientOptions(
|
||||
// var serial: String = "", // to server
|
||||
|
||||
|
||||
@@ -953,6 +953,7 @@ internal fun DeviceTile(
|
||||
val trimmedHost = currentDraft.host.trim()
|
||||
if (trimmedHost.isBlank()) return@LaunchedEffect
|
||||
val updated = DeviceShortcut(
|
||||
id = currentDraft.id,
|
||||
name = currentDraft.name.trim(),
|
||||
host = trimmedHost,
|
||||
port = currentDraft.port,
|
||||
@@ -1200,7 +1201,7 @@ internal fun DeviceTileList(
|
||||
internal fun QuickConnectCard(
|
||||
input: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
onFocusChange: (() -> Unit)? = null,
|
||||
onFocusLost: (() -> Unit)? = null,
|
||||
onConnect: () -> Unit,
|
||||
onAddDevice: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
@@ -1240,7 +1241,7 @@ internal fun QuickConnectCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
onFocusLost = onFocusChange,
|
||||
onFocusLost = onFocusLost,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
Reference in New Issue
Block a user