Use stable identifiers to locate servers (#6103)

This commit is contained in:
eliotcougar authored and GitHub committed 2026-08-23 19:33:27 +08:00
1 parent dac65a972c
commit a1b45bbfb2
5 files changed
+58 -73

No files matched your search

@@ -2,6 +2,5 @@ package com.v2ray.ang.dto
data class LocateTarget(
val groupId: String,
val groupIndex: Int,
val itemPosition: Int
)
val serverGuid: String,
)
@@ -66,5 +66,5 @@ sealed interface MainAction {
data class ImportBatchConfig(val configText: String) : MainAction
data class LocateHandled(val target: LocateTarget) : MainAction
data object LocateHandled : MainAction
}
@@ -30,7 +30,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.ui.compose.LocalDarkTheme
import com.v2ray.ang.ui.compose.QRCodeDialog
import com.v2ray.ang.extension.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@@ -73,16 +72,12 @@ fun MainScreen(
val lazyListStates = remember { mutableStateMapOf<String, LazyListState>() }
val lazyGridStates = remember { mutableStateMapOf<String, LazyGridState>() }
var locateInProgress by remember { mutableStateOf(false) }
LaunchedEffect(groups) {
val validGroupIds = groups.map { it.id }.toSet()
lazyListStates.keys.retainAll(validGroupIds)
lazyGridStates.keys.retainAll(validGroupIds)
}
val latestDoubleColumnDisplay by rememberUpdatedState(doubleColumnDisplay)
LaunchedEffect(groups, uiState.selectedGroupId) {
if (groups.isEmpty()) return@LaunchedEffect
val selectedIndex = groups.indexOfFirst { it.id == uiState.selectedGroupId }
@@ -93,68 +88,18 @@ fun MainScreen(
}
val latestGroups by rememberUpdatedState(groups)
val latestLocateInProgress by rememberUpdatedState(locateInProgress)
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.settledPage }
.distinctUntilChanged()
.collect { page ->
val currentGroups = latestGroups
if (!latestLocateInProgress && page in currentGroups.indices) {
if (page in currentGroups.indices) {
onAction(MainAction.SelectGroup(currentGroups[page].id))
}
}
}
LaunchedEffect(uiState.locateTarget) {
val target = uiState.locateTarget ?: return@LaunchedEffect
if (target.groupIndex !in 0 until pagerState.pageCount) {
mainViewModel.onAction(MainAction.LocateHandled(target))
return@LaunchedEffect
}
locateInProgress = true
try {
if (pagerState.settledPage != target.groupIndex) {
pagerState.navigateToPageOptimized(
targetPage = target.groupIndex,
animateAdjacentPage = false
)
}
onAction(MainAction.SelectGroup(target.groupId))
repeat(10) {
val ready = if (latestDoubleColumnDisplay) {
lazyGridStates[target.groupId] != null
} else {
lazyListStates[target.groupId] != null
}
if (ready) return@repeat
delay(16)
}
if (latestDoubleColumnDisplay) {
lazyGridStates[target.groupId]?.let { gridState ->
gridState.scrollToItem(
index = target.itemPosition,
scrollOffset = -gridState.layoutInfo.viewportSize.height / 3
)
}
} else {
lazyListStates[target.groupId]?.let { listState ->
listState.scrollToItem(
index = target.itemPosition,
scrollOffset = -listState.layoutInfo.viewportSize.height / 3
)
}
}
} finally {
delay(32)
locateInProgress = false
mainViewModel.onAction(MainAction.LocateHandled(target))
}
}
MainDialogs(
showDelAllConfirm = showDelAllConfirm,
onDismissDelAll = { showDelAllConfirm = false },
@@ -279,6 +224,7 @@ fun MainScreen(
groupId = group.id,
mainViewModel = mainViewModel,
selectedGuid = selectedGuid,
locateTarget = uiState.locateTarget,
doubleColumnDisplay = doubleColumnDisplay,
confirmRemove = confirmRemove,
searchQuery = searchQuery,
@@ -30,6 +30,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -46,6 +47,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.v2ray.ang.R
import com.v2ray.ang.dto.LocateTarget
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.dto.entities.ServersCache
import com.v2ray.ang.extension.isComplexType
@@ -69,6 +71,7 @@ fun GroupPagerPage(
groupId: String,
mainViewModel: MainViewModel,
selectedGuid: String?,
locateTarget: LocateTarget?,
doubleColumnDisplay: Boolean,
confirmRemove: Boolean,
searchQuery: String,
@@ -89,6 +92,7 @@ fun GroupPagerPage(
ServerListPage(
servers = servers,
selectedGuid = selectedGuid,
locateTarget = locateTarget?.takeIf { it.groupId == groupId },
canReorder = canReorder,
doubleColumnDisplay = doubleColumnDisplay,
subscriptionId = groupId,
@@ -101,6 +105,7 @@ fun GroupPagerPage(
onShareServer = onShareServer,
onMoreServer = onMoreServer,
onRemoveServer = onRemoveServer,
onLocateHandled = { mainViewModel.onAction(MainAction.LocateHandled) },
onMoveServer = { fromIndex, toIndex -> mainViewModel.moveServer(groupId, fromIndex, toIndex) },
contentPadding = contentPadding
)
@@ -110,6 +115,7 @@ fun GroupPagerPage(
private fun ServerListPage(
servers: List<ServersCache>,
selectedGuid: String?,
locateTarget: LocateTarget?,
canReorder: Boolean,
doubleColumnDisplay: Boolean,
subscriptionId: String,
@@ -122,6 +128,7 @@ private fun ServerListPage(
onShareServer: (String, ProfileItem) -> Unit,
onMoreServer: (String, ProfileItem) -> Unit,
onRemoveServer: (String) -> Unit,
onLocateHandled: () -> Unit,
onMoveServer: (Int, Int) -> Unit,
contentPadding: PaddingValues
) {
@@ -135,6 +142,8 @@ private fun ServerListPage(
}
} else null
LocateTargetEffect(locateTarget, servers, gridState, onLocateHandled)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
state = gridState,
@@ -182,6 +191,8 @@ private fun ServerListPage(
}
} else null
LocateTargetEffect(locateTarget, servers, listState, onLocateHandled)
LazyColumn(
state = listState,
modifier = Modifier
@@ -230,6 +241,38 @@ private fun ServerListPage(
}
}
@Composable
private fun LocateTargetEffect(
target: LocateTarget?,
servers: List<ServersCache>,
state: LazyListState,
onHandled: () -> Unit,
) {
if (target == null) return
LaunchedEffect(target, servers) {
val index = servers.indexOfFirst { it.guid == target.serverGuid }
if (index < 0) return@LaunchedEffect
state.scrollToItem(index, -state.layoutInfo.viewportSize.height / 3)
onHandled()
}
}
@Composable
private fun LocateTargetEffect(
target: LocateTarget?,
servers: List<ServersCache>,
state: LazyGridState,
onHandled: () -> Unit,
) {
if (target == null) return
LaunchedEffect(target, servers) {
val index = servers.indexOfFirst { it.guid == target.serverGuid }
if (index < 0) return@LaunchedEffect
state.scrollToItem(index, -state.layoutInfo.viewportSize.height / 3)
onHandled()
}
}
@Composable
private fun ServerItemRow(
serverCache: ServersCache,
@@ -191,7 +191,7 @@ class MainViewModel(
is MainAction.RemoveServer -> removeServerAndRefresh(action.guid)
is MainAction.Search -> filterConfig(action.query)
is MainAction.ImportBatchConfig -> importBatchConfig(action.configText)
is MainAction.LocateHandled -> consumeLocateTarget(action.target)
MainAction.LocateHandled -> consumeLocateTarget()
is MainAction.ShareQRCode -> {
val bitmap = dataSource.share2QRCode(action.guid)
_uiState.update { it.copy(shareQRCodeBitmap = bitmap) }
@@ -744,24 +744,21 @@ class MainViewModel(
val selected = dataSource.getSelectServer() ?: return
val profile = dataSource.decodeServerConfig(selected) ?: return
val groupId = profile.subscriptionId
val groupIndex =
_uiState.value.groups.indexOfFirst { it.id == groupId }.takeIf { it >= 0 } ?: return
if (_uiState.value.groups.none { it.id == groupId }) return
viewModelScope.launch(ioDispatcher) {
val position =
loadGroup(groupId).indexOfFirst { it.guid == selected }.takeIf { it >= 0 }
?: return@launch
updateGroupUi(groupId, loadGroup(groupId))
if (_uiState.value.selectedGroupId != groupId) {
dataSource.setSelectedSubscriptionId(groupId)
}
val target = LocateTarget(groupId, selected)
_uiState.update {
it.copy(locateTarget = LocateTarget(groupId, groupIndex, position))
it.copy(selectedGroupId = groupId, locateTarget = target)
}
}
}
fun getPosition(guid: String): Int = currentServers().indexOfFirst { it.guid == guid }
private fun consumeLocateTarget(target: LocateTarget) {
_uiState.update { state ->
if (state.locateTarget == target) state.copy(locateTarget = null) else state
}
private fun consumeLocateTarget() {
_uiState.update { it.copy(locateTarget = null) }
}
// ---------- Running state ----------