diff --git a/README.md b/README.md index 533a068..0799870 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ ## Features +- 带生物认证的锁屏密码自动填充 (入口位于虚拟按钮中) - 可替换 scrcpy-server - 利用 mDNS 服务实现自动连接启用无线调试的设备、自动发现等待配对设备的IP与端口 - 自动横竖屏切换(算吗 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fd5e651..3de4cea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { 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 fda22c7..6b57c11 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 @@ -19,8 +19,24 @@ class DeviceShortcuts(val devices: List) : List 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) : List 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) : List 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) : List 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, 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 a63e9d7..7eaa6e7 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 @@ -185,7 +185,7 @@ fun DeviceTabPage( scrcpy: Scrcpy, ) { val activity = LocalActivity.current - val fragmentActivity = remember(activity) {activity as? FragmentActivity} + val fragmentActivity = remember(activity) { activity as? FragmentActivity } val context = LocalContext.current val scope = rememberCoroutineScope() @@ -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) }, ) diff --git a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt index 11d72ff..dad468f 100644 --- a/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt +++ b/app/src/main/java/io/github/miuzarte/scrcpyforandroid/pages/SettingsScreen.kt @@ -492,7 +492,6 @@ fun SettingsPage( item { SectionSmallTitle("") Card { - // TODO: 进入时无视自动更新检查的 CD, 主动触发一次 ArrowPreference( title = "关于", summary = updateSummary, 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 bd9a4c6..a8fb335 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 @@ -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 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 6d74230..31ba23d 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 @@ -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(),