Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ 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.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand All @@ -35,6 +39,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand All @@ -61,6 +66,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.BaselineArrowUp
import com.github.kr328.clash.ui.icon.BaselineFlashOn
import com.github.kr328.clash.ui.icon.BaselineMoreVert
import com.github.kr328.clash.ui.icon.TabbyIcons
Expand Down Expand Up @@ -126,6 +132,45 @@ private fun ProxyContent(
var menuVisible by remember { mutableStateOf(false) }
val currentGroup = uiState.groups.getOrNull(uiState.currentPage)
val showUrlTestAction = uiState.groupNames.isNotEmpty()
val groupNames = uiState.groupNames
val pagerState =
if (groupNames.isNotEmpty()) {
val initialPage = uiState.initialPage.coerceIn(groupNames.indices)
rememberPagerState(initialPage = initialPage, pageCount = { groupNames.size })
} else {
null
}
val scope = rememberCoroutineScope()
val gridStates =
if (groupNames.isNotEmpty()) {
List(groupNames.size) { rememberLazyGridState() }
} else {
emptyList()
}

pagerState?.let { validPagerState ->
LaunchedEffect(validPagerState) {
snapshotFlow { validPagerState.currentPage }.collect(onPageChanged)
}

val currentPage = uiState.currentPage.coerceIn(groupNames.indices)
LaunchedEffect(currentPage) {
if (currentPage != validPagerState.currentPage) validPagerState.scrollToPage(currentPage)
}
}

val firstRowSize = columnsForProxyLine(uiState.proxyLine)
val showScrollToTopFab by
remember(pagerState, gridStates, firstRowSize) {
derivedStateOf {
val validPagerState = pagerState ?: return@derivedStateOf false
val currentGridState =
gridStates.getOrNull(validPagerState.currentPage) ?: return@derivedStateOf false

!currentGridState.isScrollInProgress &&
currentGridState.firstVisibleItemIndex >= firstRowSize
}
}

if (menuVisible) {
ModalBottomSheet(
Expand Down Expand Up @@ -185,6 +230,20 @@ private fun ProxyContent(
)
}
},
floatingActionButton = {
if (showScrollToTopFab) {
FloatingActionButton(
onClick = {
val validPagerState = pagerState ?: return@FloatingActionButton
val currentGridState =
gridStates.getOrNull(validPagerState.currentPage) ?: return@FloatingActionButton
scope.launch { currentGridState.animateScrollToItem(0) }
}
) {
Icon(imageVector = TabbyIcons.BaselineArrowUp, contentDescription = null)
}
}
},
) { innerPadding ->
Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
if (uiState.groupNames.isEmpty()) {
Expand All @@ -198,7 +257,8 @@ private fun ProxyContent(
ProxyPagerContent(
uiState = uiState,
selectedProxies = selectedProxies,
onPageChanged = onPageChanged,
pagerState = pagerState ?: return@Box,
gridStates = gridStates,
onProxySelected = onProxySelected,
)
}
Expand All @@ -210,23 +270,15 @@ private fun ProxyContent(
private fun ProxyPagerContent(
uiState: ProxyViewModel.UiState,
selectedProxies: List<SelectedProxy>,
onPageChanged: (Int) -> Unit,
pagerState: PagerState,
gridStates: List<LazyGridState>,
onProxySelected: (Int, String) -> Unit,
) {
val groupNames = uiState.groupNames
if (groupNames.isEmpty()) return

val initialPage = uiState.initialPage.coerceIn(groupNames.indices)
val pagerState = rememberPagerState(initialPage = initialPage, pageCount = { groupNames.size })
val scope = rememberCoroutineScope()

LaunchedEffect(pagerState) { snapshotFlow { pagerState.currentPage }.collect(onPageChanged) }

val currentPage = uiState.currentPage.coerceIn(groupNames.indices)
LaunchedEffect(currentPage) {
if (currentPage != pagerState.currentPage) pagerState.scrollToPage(currentPage)
}

Column(modifier = Modifier.fillMaxSize()) {
PrimaryScrollableTabRow(selectedTabIndex = pagerState.currentPage, edgePadding = 0.dp) {
groupNames.forEachIndexed { index, name ->
Expand All @@ -245,6 +297,7 @@ private fun ProxyPagerContent(
index = page,
proxyLine = uiState.proxyLine,
group = uiState.groups.getOrNull(page) ?: ProxyViewModel.UiState.ProxyGroupUiState(),
gridState = gridStates.getOrElse(page) { rememberLazyGridState() },
selectedProxies = selectedProxies,
onProxySelected = onProxySelected,
)
Expand All @@ -257,6 +310,7 @@ private fun ProxyGroupPage(
index: Int,
proxyLine: Int,
group: ProxyViewModel.UiState.ProxyGroupUiState,
gridState: LazyGridState,
selectedProxies: List<SelectedProxy>,
onProxySelected: (Int, String) -> Unit,
) {
Expand All @@ -268,6 +322,7 @@ private fun ProxyGroupPage(
val unselectedBackground = MaterialTheme.colorScheme.surface

LazyVerticalGrid(
state = gridState,
columns = GridCells.Fixed(columnsForProxyLine(proxyLine)),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(gridContentPadding),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,14 @@ fun TabbyScaffold(
snackbarHost: @Composable () -> Unit = {
snackbarHostState?.let { SnackbarHost(hostState = it) }
},
floatingActionButton: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit,
) {
Scaffold(modifier = modifier, topBar = topBar, snackbarHost = snackbarHost, content = content)
Scaffold(
modifier = modifier,
topBar = topBar,
snackbarHost = snackbarHost,
floatingActionButton = floatingActionButton,
content = content,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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.BaselineArrowUp: ImageVector
get() {
if (_BaselineArrowUp != null) {
return _BaselineArrowUp!!
}
_BaselineArrowUp =
ImageVector.Builder(
name = "arrow-up",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 16f,
viewportHeight = 16f,
)
.apply {
path(fill = SolidColor(Color.White)) {
moveTo(8f, 15f)
arcToRelative(0.5f, 0.5f, 0f, false, false, 0.5f, -0.5f)
verticalLineTo(2.707f)
lineToRelative(3.146f, 3.147f)
arcToRelative(0.5f, 0.5f, 0f, false, false, 0.708f, -0.708f)
lineToRelative(-4f, -4f)
arcToRelative(0.5f, 0.5f, 0f, false, false, -0.708f, 0f)
lineToRelative(-4f, 4f)
arcToRelative(0.5f, 0.5f, 0f, true, false, 0.708f, 0.708f)
lineTo(7.5f, 2.707f)
verticalLineTo(14.5f)
arcToRelative(0.5f, 0.5f, 0f, false, false, 0.5f, 0.5f)
}
}
.build()
return _BaselineArrowUp!!
}

@Suppress("ObjectPropertyName") private var _BaselineArrowUp: ImageVector? = null
4 changes: 4 additions & 0 deletions ui/src/main/kotlin/com/github/kr328/clash/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ private val DarkColorScheme =
darkColorScheme(
primary = TabbyDarkPrimary,
onPrimary = TabbyOnPrimary,
primaryContainer = TabbyDarkPrimary,
onPrimaryContainer = TabbyOnPrimary,
secondary = TabbyDarkPrimary,
onSecondary = TabbyOnPrimary,
background = TabbyDarkBackground,
Expand All @@ -45,6 +47,8 @@ private val LightColorScheme =
lightColorScheme(
primary = TabbyLightPrimary,
onPrimary = TabbyOnPrimary,
primaryContainer = TabbyLightPrimary,
onPrimaryContainer = TabbyOnPrimary,
secondary = TabbyLightPrimary,
onSecondary = TabbyOnPrimary,
background = TabbyLightBackground,
Expand Down