From 12ccb1ef100dfc639230d650080b3c52a7b49e3d Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 14 May 2026 20:36:12 +0800 Subject: [PATCH] Add center selected functionality and update localization strings --- .../kr328/clash/proxy/ui/ProxyScreen.kt | 44 +++++++++++++++++++ .../src/main/res/values-ja-rJP/strings.xml | 1 + .../src/main/res/values-ko-rKR/strings.xml | 1 + ui/proxy/src/main/res/values-ru/strings.xml | 1 + ui/proxy/src/main/res/values-vi/strings.xml | 1 + .../src/main/res/values-zh-rHK/strings.xml | 1 + .../src/main/res/values-zh-rTW/strings.xml | 1 + ui/proxy/src/main/res/values-zh/strings.xml | 1 + ui/proxy/src/main/res/values/strings.xml | 1 + .../clash/ui/icon/BaselineCircleCenter.kt | 42 ++++++++++++++++++ 10 files changed, 94 insertions(+) create mode 100644 ui/src/main/kotlin/com/github/kr328/clash/ui/icon/BaselineCircleCenter.kt diff --git a/ui/proxy/src/main/kotlin/com/github/kr328/clash/proxy/ui/ProxyScreen.kt b/ui/proxy/src/main/kotlin/com/github/kr328/clash/proxy/ui/ProxyScreen.kt index 3288e7a486..5dd3dd2a35 100644 --- a/ui/proxy/src/main/kotlin/com/github/kr328/clash/proxy/ui/ProxyScreen.kt +++ b/ui/proxy/src/main/kotlin/com/github/kr328/clash/proxy/ui/ProxyScreen.kt @@ -3,6 +3,7 @@ package com.github.kr328.clash.proxy.ui import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,7 +15,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.selection.selectable @@ -61,6 +64,7 @@ import com.github.kr328.clash.proxy.vm.ProxyViewModel import com.github.kr328.clash.proxy.vm.ProxyViewModel.SelectedProxy import com.github.kr328.clash.ui.component.Spacer import com.github.kr328.clash.ui.component.TabbyScaffold +import com.github.kr328.clash.ui.icon.BaselineCircleCenter import com.github.kr328.clash.ui.icon.BaselineFlashOn import com.github.kr328.clash.ui.icon.BaselineMoreVert import com.github.kr328.clash.ui.icon.TabbyIcons @@ -124,6 +128,7 @@ private fun ProxyContent( onProxySelected: (Int, String) -> Unit, ) { var menuVisible by remember { mutableStateOf(false) } + var centerSelectedRequestVersion by remember { mutableStateOf(0) } val currentGroup = uiState.groups.getOrNull(uiState.currentPage) val showUrlTestAction = uiState.groupNames.isNotEmpty() @@ -178,6 +183,13 @@ private fun ProxyContent( } } + IconButton(onClick = { centerSelectedRequestVersion += 1 }) { + Icon( + imageVector = TabbyIcons.BaselineCircleCenter, + contentDescription = stringResource(R.string.center_selected), + ) + } + IconButton(onClick = { menuVisible = true }) { Icon( imageVector = TabbyIcons.BaselineMoreVert, @@ -198,6 +210,7 @@ private fun ProxyContent( ProxyPagerContent( uiState = uiState, selectedProxies = selectedProxies, + centerSelectedRequestVersion = centerSelectedRequestVersion, onPageChanged = onPageChanged, onProxySelected = onProxySelected, ) @@ -210,6 +223,7 @@ private fun ProxyContent( private fun ProxyPagerContent( uiState: ProxyViewModel.UiState, selectedProxies: List, + centerSelectedRequestVersion: Int, onPageChanged: (Int) -> Unit, onProxySelected: (Int, String) -> Unit, ) { @@ -245,6 +259,9 @@ private fun ProxyPagerContent( index = page, proxyLine = uiState.proxyLine, group = uiState.groups.getOrNull(page) ?: ProxyViewModel.UiState.ProxyGroupUiState(), + selectedProxyName = selectedProxies.getOrNull(page)?.name, + isCurrentPage = page == pagerState.currentPage, + centerSelectedRequestVersion = centerSelectedRequestVersion, selectedProxies = selectedProxies, onProxySelected = onProxySelected, ) @@ -257,17 +274,31 @@ private fun ProxyGroupPage( index: Int, proxyLine: Int, group: ProxyViewModel.UiState.ProxyGroupUiState, + selectedProxyName: String?, + isCurrentPage: Boolean, + centerSelectedRequestVersion: Int, selectedProxies: List, onProxySelected: (Int, String) -> Unit, ) { val sources = group.sources val refreshVersion = group.refreshVersion + val gridState = rememberLazyGridState() val selectedControl = MaterialTheme.colorScheme.onPrimary val selectedBackground = MaterialTheme.colorScheme.primary val unselectedControl = MaterialTheme.colorScheme.onSurface val unselectedBackground = MaterialTheme.colorScheme.surface + LaunchedEffect(centerSelectedRequestVersion, isCurrentPage, selectedProxyName, sources) { + if (!isCurrentPage || centerSelectedRequestVersion == 0) return@LaunchedEffect + + val selectedIndex = sources.indexOfFirst { it.proxy.name == selectedProxyName } + if (selectedIndex < 0) return@LaunchedEffect + + gridState.animateScrollItemToCenter(selectedIndex) + } + LazyVerticalGrid( + state = gridState, columns = GridCells.Fixed(columnsForProxyLine(proxyLine)), modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(gridContentPadding), @@ -487,6 +518,19 @@ private fun columnsForProxyLine(proxyLine: Int): Int = else -> 3 } +private suspend fun LazyGridState.animateScrollItemToCenter(index: Int) { + animateScrollToItem(index) + + val selectedItem = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index } ?: return + val viewportCenter = (layoutInfo.viewportStartOffset + layoutInfo.viewportEndOffset) / 2 + val itemCenter = selectedItem.offset.y + selectedItem.size.height / 2 + val delta = itemCenter - viewportCenter + + if (delta != 0) { + animateScrollBy(delta.toFloat()) + } +} + @PreviewWrapper(TabbyThemeWrapper::class) @PreviewTabby @Composable diff --git a/ui/proxy/src/main/res/values-ja-rJP/strings.xml b/ui/proxy/src/main/res/values-ja-rJP/strings.xml index 15aa1552e1..3804781a9f 100644 --- a/ui/proxy/src/main/res/values-ja-rJP/strings.xml +++ b/ui/proxy/src/main/res/values-ja-rJP/strings.xml @@ -1,6 +1,7 @@ レイテンシ + 選択項目を中央に表示 接続テスト ダブルカラム レイアウト diff --git a/ui/proxy/src/main/res/values-ko-rKR/strings.xml b/ui/proxy/src/main/res/values-ko-rKR/strings.xml index 347f957114..1d4d1a0db4 100644 --- a/ui/proxy/src/main/res/values-ko-rKR/strings.xml +++ b/ui/proxy/src/main/res/values-ko-rKR/strings.xml @@ -1,6 +1,7 @@ 지연 수치 + 선택 항목 가운데로 이동 지연 수치 테스트 더블 레이아웃 diff --git a/ui/proxy/src/main/res/values-ru/strings.xml b/ui/proxy/src/main/res/values-ru/strings.xml index f4d4dc87b7..315da66e53 100644 --- a/ui/proxy/src/main/res/values-ru/strings.xml +++ b/ui/proxy/src/main/res/values-ru/strings.xml @@ -1,6 +1,7 @@ Задержка + По центру к выбранному Проверка задержки Двойное Отображение diff --git a/ui/proxy/src/main/res/values-vi/strings.xml b/ui/proxy/src/main/res/values-vi/strings.xml index d661a36b9a..aee21faef9 100644 --- a/ui/proxy/src/main/res/values-vi/strings.xml +++ b/ui/proxy/src/main/res/values-vi/strings.xml @@ -1,6 +1,7 @@ Độ trễ + Đưa mục đã chọn vào giữa Kiểm tra độ trễ Hai cột Bố cục diff --git a/ui/proxy/src/main/res/values-zh-rHK/strings.xml b/ui/proxy/src/main/res/values-zh-rHK/strings.xml index cd5bacfdcf..9cb46543da 100644 --- a/ui/proxy/src/main/res/values-zh-rHK/strings.xml +++ b/ui/proxy/src/main/res/values-zh-rHK/strings.xml @@ -1,6 +1,7 @@ 延遲 + 置中目前選擇 延遲測試 雙列 佈局 diff --git a/ui/proxy/src/main/res/values-zh-rTW/strings.xml b/ui/proxy/src/main/res/values-zh-rTW/strings.xml index 69b7a7b7ba..852b6012e1 100644 --- a/ui/proxy/src/main/res/values-zh-rTW/strings.xml +++ b/ui/proxy/src/main/res/values-zh-rTW/strings.xml @@ -1,6 +1,7 @@ 延遲 + 置中目前選擇 延遲測試 雙欄 佈局 diff --git a/ui/proxy/src/main/res/values-zh/strings.xml b/ui/proxy/src/main/res/values-zh/strings.xml index 93fff1b05f..0652bb0ada 100644 --- a/ui/proxy/src/main/res/values-zh/strings.xml +++ b/ui/proxy/src/main/res/values-zh/strings.xml @@ -1,6 +1,7 @@ 延迟 + 居中当前选择 延迟测试 双列 布局 diff --git a/ui/proxy/src/main/res/values/strings.xml b/ui/proxy/src/main/res/values/strings.xml index 691c3eb658..85c4b2bf56 100644 --- a/ui/proxy/src/main/res/values/strings.xml +++ b/ui/proxy/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ Delay + Center Selected Delay Test Double Layout diff --git a/ui/src/main/kotlin/com/github/kr328/clash/ui/icon/BaselineCircleCenter.kt b/ui/src/main/kotlin/com/github/kr328/clash/ui/icon/BaselineCircleCenter.kt new file mode 100644 index 0000000000..f8250d70ac --- /dev/null +++ b/ui/src/main/kotlin/com/github/kr328/clash/ui/icon/BaselineCircleCenter.kt @@ -0,0 +1,42 @@ +package com.github.kr328.clash.ui.icon + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +@Suppress("UnusedReceiverParameter") +val TabbyIcons.BaselineCircleCenter: ImageVector + get() { + if (_BaselineCircleCenter != null) return _BaselineCircleCenter!! + + _BaselineCircleCenter = + ImageVector.Builder( + name = "record-circle", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 16f, + viewportHeight = 16f, + ) + .apply { + path(fill = SolidColor(Color.White)) { + moveTo(8f, 15f) + arcTo(7f, 7f, 0f, true, true, 8f, 1f) + arcToRelative(7f, 7f, 0f, false, true, 0f, 14f) + moveToRelative(0f, 1f) + arcTo(8f, 8f, 0f, true, false, 8f, 0f) + arcToRelative(8f, 8f, 0f, false, false, 0f, 16f) + } + path(fill = SolidColor(Color.White)) { + moveTo(11f, 8f) + arcToRelative(3f, 3f, 0f, true, true, -6f, 0f) + arcToRelative(3f, 3f, 0f, false, true, 6f, 0f) + } + } + .build() + + return _BaselineCircleCenter!! + } + +@Suppress("ObjectPropertyName") private var _BaselineCircleCenter: ImageVector? = null