diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ProxyChainMembers.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ProxyChainMembers.kt new file mode 100644 index 00000000..e01f59f9 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ProxyChainMembers.kt @@ -0,0 +1,14 @@ +package com.v2ray.ang.ui.server + +/** Removes one draft member and its row key together, resolving its current position at confirmation. */ +internal fun withoutProxyChainMember( + members: List, + memberKeys: List, + memberKey: String, +): Pair, List> { + val index = memberKeys.indexOf(memberKey) + if (index < 0) return members to memberKeys + + return members.toMutableList().also { it.removeAt(index) } to + memberKeys.toMutableList().also { it.removeAt(index) } +} diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerProxyChainActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerProxyChainActivity.kt index fbb99ac7..a0e1acc6 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerProxyChainActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/server/ServerProxyChainActivity.kt @@ -203,7 +203,7 @@ fun ProxyChainScreen( var members by rememberSaveable { mutableStateOf(initialMembers) } var memberKeys by rememberSaveable { mutableStateOf(List(initialMembers.size) { UUID.randomUUID().toString() }) } var showProfileDeleteConfirm by remember { mutableStateOf(false) } - var memberToDeleteIndex by rememberSaveable { mutableStateOf(null) } + var memberToDeleteKey by rememberSaveable { mutableStateOf(null) } val showDelete = editGuid.isNotEmpty() && !isRunning val lazyListState = rememberLazyListState() @@ -285,7 +285,8 @@ fun ProxyChainScreen( } itemsIndexed(items = members, key = { index, _ -> memberKeys[index] }) { index, member -> - ReorderableItem(reorderableState, key = memberKeys[index]) { isDragging -> + val memberKey = memberKeys[index] + ReorderableItem(reorderableState, key = memberKey) { isDragging -> val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp) Surface(shadowElevation = elevation) { Row( @@ -314,10 +315,11 @@ fun ProxyChainScreen( ) IconButton(onClick = { if (member.isBlank()) { - members = members.toMutableList().also { it.removeAt(index) } - memberKeys = memberKeys.toMutableList().also { it.removeAt(index) } + val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, memberKeys, memberKey) + members = remainingMembers + memberKeys = remainingKeys } else { - memberToDeleteIndex = index + memberToDeleteKey = memberKey } }) { Icon( @@ -339,15 +341,16 @@ fun ProxyChainScreen( onDismiss = { showProfileDeleteConfirm = false } ) } - memberToDeleteIndex?.let { index -> + memberToDeleteKey?.let { memberKey -> DeleteConfirmDialog( message = stringResource(R.string.confirm_delete_proxy_chain_member), onConfirm = { - members = members.toMutableList().also { it.removeAt(index) } - memberKeys = memberKeys.toMutableList().also { it.removeAt(index) } - memberToDeleteIndex = null + val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, memberKeys, memberKey) + members = remainingMembers + memberKeys = remainingKeys + memberToDeleteKey = null }, - onDismiss = { memberToDeleteIndex = null } + onDismiss = { memberToDeleteKey = null } ) } } diff --git a/V2rayNG/app/src/test/java/com/v2ray/ang/ui/server/ProxyChainMembersTest.kt b/V2rayNG/app/src/test/java/com/v2ray/ang/ui/server/ProxyChainMembersTest.kt new file mode 100644 index 00000000..96b7c3a2 --- /dev/null +++ b/V2rayNG/app/src/test/java/com/v2ray/ang/ui/server/ProxyChainMembersTest.kt @@ -0,0 +1,60 @@ +package com.v2ray.ang.ui.server + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Test + +class ProxyChainMembersTest { + @Test + fun removalFollowsThePendingKeyAfterReordering() { + val pendingKey = "two" + val members = listOf("Two", "One", "Three") + val keys = listOf("two", "one", "three") + + val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, keys, pendingKey) + + assertEquals(listOf("One", "Three"), remainingMembers) + assertEquals(listOf("one", "three"), remainingKeys) + assertEquals(listOf("Two", "One", "Three"), members) + assertEquals(listOf("two", "one", "three"), keys) + } + + @Test + fun duplicateNamesDoNotChangeWhichMemberIsRemoved() { + val (members, keys) = withoutProxyChainMember( + listOf("Same", "Same", "Other"), + listOf("first", "second", "third"), + "second", + ) + + assertEquals(listOf("Same", "Other"), members) + assertEquals(listOf("first", "third"), keys) + } + + @Test + fun missingMemberLeavesBothListsUnchanged() { + val members = listOf("One", "Three") + val keys = listOf("one", "three") + + val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, keys, "two") + + assertSame(members, remainingMembers) + assertSame(keys, remainingKeys) + } + + @Test + fun emptyChainRemainsEmpty() { + assertEquals( + emptyList() to emptyList(), + withoutProxyChainMember(emptyList(), emptyList(), "missing"), + ) + } + + @Test + fun blankMemberIsRemovedWithItsOwnKey() { + assertEquals( + listOf("One") to listOf("one"), + withoutProxyChainMember(listOf("One", ""), listOf("one", "blank"), "blank"), + ) + } +}