Unify sheets swipe-to-dismiss gating
This commit is contained in:
@@ -1014,46 +1014,47 @@ class MainActivity :
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.9f),
|
||||
.fillMaxHeight(),
|
||||
) {
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.title_groups),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
if (groupsUiState.groups.isNotEmpty()) {
|
||||
IconButton(onClick = { groupsViewModel.toggleAllGroups() }) {
|
||||
Icon(
|
||||
imageVector = if (allCollapsed) {
|
||||
Icons.Default.UnfoldMore
|
||||
} else {
|
||||
Icons.Default.UnfoldLess
|
||||
},
|
||||
contentDescription = if (allCollapsed) {
|
||||
stringResource(R.string.expand_all)
|
||||
} else {
|
||||
stringResource(R.string.collapse_all)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Groups content
|
||||
GroupsCard(
|
||||
serviceStatus = currentServiceStatus,
|
||||
commandClient = dashboardViewModel.commandClient,
|
||||
viewModel = groupsViewModel,
|
||||
listHeaderContent = {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.title_groups),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
if (groupsUiState.groups.isNotEmpty()) {
|
||||
IconButton(onClick = { groupsViewModel.toggleAllGroups() }) {
|
||||
Icon(
|
||||
imageVector = if (allCollapsed) {
|
||||
Icons.Default.UnfoldMore
|
||||
} else {
|
||||
Icons.Default.UnfoldLess
|
||||
},
|
||||
contentDescription = if (allCollapsed) {
|
||||
stringResource(R.string.expand_all)
|
||||
} else {
|
||||
stringResource(R.string.collapse_all)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
asSheet = true,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
@@ -1097,7 +1098,7 @@ class MainActivity :
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.9f),
|
||||
.fillMaxHeight(),
|
||||
) {
|
||||
if (displayConnection != null) {
|
||||
ConnectionDetailsScreen(
|
||||
@@ -1106,11 +1107,13 @@ class MainActivity :
|
||||
onClose = {
|
||||
selectedConnectionId?.let { connectionsViewModel.closeConnection(it) }
|
||||
},
|
||||
asSheet = true,
|
||||
)
|
||||
} else {
|
||||
ConnectionsPage(
|
||||
serviceStatus = currentServiceStatus,
|
||||
viewModel = connectionsViewModel,
|
||||
asSheet = true,
|
||||
showTitle = true,
|
||||
onConnectionClick = { selectedConnectionId = it },
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberOverscrollEffect
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -46,6 +47,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.compose.model.Connection
|
||||
import io.nekohasekai.sfa.compose.util.rememberSheetDismissFromContentOnlyIfGestureStartedAtTopModifier
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
@@ -57,17 +59,25 @@ fun ConnectionDetailsScreen(
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
showHeader: Boolean = true,
|
||||
asSheet: Boolean = false,
|
||||
) {
|
||||
val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
val scrollState = rememberScrollState()
|
||||
val bounceBlockingConnection = rememberBounceBlockingNestedScrollConnection(scrollState)
|
||||
val scrollModifier =
|
||||
if (asSheet) {
|
||||
rememberSheetDismissFromContentOnlyIfGestureStartedAtTopModifier {
|
||||
scrollState.value == 0
|
||||
}
|
||||
} else {
|
||||
Modifier.nestedScroll(rememberBounceBlockingNestedScrollConnection(scrollState))
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(bounceBlockingConnection)
|
||||
.verticalScroll(scrollState),
|
||||
.then(scrollModifier)
|
||||
.verticalScroll(scrollState, overscrollEffect = if (asSheet) null else rememberOverscrollEffect()),
|
||||
) {
|
||||
if (showHeader) {
|
||||
Row(
|
||||
|
||||
@@ -17,7 +17,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberOverscrollEffect
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
@@ -44,6 +44,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -62,6 +63,7 @@ import io.nekohasekai.sfa.compose.model.Connection
|
||||
import io.nekohasekai.sfa.compose.model.ConnectionSort
|
||||
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.compose.util.rememberSheetDismissFromContentOnlyIfGestureStartedAtTopModifier
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -69,6 +71,7 @@ import io.nekohasekai.sfa.constant.Status
|
||||
fun ConnectionsPage(
|
||||
serviceStatus: Status,
|
||||
viewModel: ConnectionsViewModel = viewModel(),
|
||||
asSheet: Boolean = false,
|
||||
showTitle: Boolean = true,
|
||||
showTopBar: Boolean = false,
|
||||
onConnectionClick: (String) -> Unit = {},
|
||||
@@ -87,14 +90,21 @@ fun ConnectionsPage(
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
val headerRowModifier =
|
||||
if (asSheet) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
} else {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 16.dp),
|
||||
.padding(bottom = 16.dp)
|
||||
}
|
||||
|
||||
val headerContent: @Composable () -> Unit = {
|
||||
Row(
|
||||
modifier = headerRowModifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@@ -258,13 +268,29 @@ fun ConnectionsPage(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (asSheet) {
|
||||
ConnectionsScreen(
|
||||
serviceStatus = serviceStatus,
|
||||
viewModel = viewModel,
|
||||
onConnectionClick = { connection -> onConnectionClick(connection.id) },
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
listHeaderContent = headerContent,
|
||||
asSheet = true,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
headerContent()
|
||||
ConnectionsScreen(
|
||||
serviceStatus = serviceStatus,
|
||||
viewModel = viewModel,
|
||||
onConnectionClick = { connection -> onConnectionClick(connection.id) },
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,6 +373,8 @@ fun ConnectionsScreen(
|
||||
serviceStatus: Status,
|
||||
viewModel: ConnectionsViewModel = viewModel(),
|
||||
onConnectionClick: (Connection) -> Unit = {},
|
||||
listHeaderContent: (@Composable () -> Unit)? = null,
|
||||
asSheet: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
@@ -365,73 +393,96 @@ fun ConnectionsScreen(
|
||||
viewModel.updateServiceStatus(serviceStatus)
|
||||
}
|
||||
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
AnimatedVisibility(
|
||||
visible = uiState.isSearchActive,
|
||||
enter = expandVertically() + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val lazyListState = rememberSaveable(saver = LazyListState.Saver) { LazyListState() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
if (asSheet) {
|
||||
val sheetSwipeToDismissModifier =
|
||||
rememberSheetDismissFromContentOnlyIfGestureStartedAtTopModifier {
|
||||
lazyListState.firstVisibleItemIndex == 0 &&
|
||||
lazyListState.firstVisibleItemScrollOffset == 0
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.searchText,
|
||||
onValueChange = { viewModel.setSearchText(it) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 8.dp)
|
||||
.focusRequester(focusRequester),
|
||||
placeholder = { Text(stringResource(R.string.search_connections)) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
trailingIcon = {
|
||||
if (uiState.searchText.isNotEmpty()) {
|
||||
IconButton(onClick = { viewModel.setSearchText("") }) {
|
||||
Icon(Icons.Default.Clear, contentDescription = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
|
||||
when {
|
||||
uiState.isLoading -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
LazyColumn(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.then(sheetSwipeToDismissModifier),
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
overscrollEffect = null,
|
||||
) {
|
||||
if (listHeaderContent != null) {
|
||||
item(key = "connections_list_header") {
|
||||
listHeaderContent()
|
||||
}
|
||||
}
|
||||
|
||||
uiState.connections.isEmpty() -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
item(key = "connections_search") {
|
||||
AnimatedVisibility(
|
||||
visible = uiState.isSearchActive,
|
||||
enter = expandVertically() + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.empty_connections),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.searchText,
|
||||
onValueChange = { viewModel.setSearchText(it) },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
.focusRequester(focusRequester),
|
||||
placeholder = { Text(stringResource(R.string.search_connections)) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
trailingIcon = {
|
||||
if (uiState.searchText.isNotEmpty()) {
|
||||
IconButton(onClick = { viewModel.setSearchText("") }) {
|
||||
Icon(Icons.Default.Clear, contentDescription = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val bounceBlockingConnection = rememberBounceBlockingNestedScrollConnection(lazyListState)
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(bounceBlockingConnection),
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
when {
|
||||
uiState.isLoading -> {
|
||||
item(key = "connections_loading") {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 48.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiState.connections.isEmpty() -> {
|
||||
item(key = "connections_empty") {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 48.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.empty_connections),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
items(
|
||||
items = uiState.connections,
|
||||
key = { it.id },
|
||||
@@ -445,6 +496,90 @@ fun ConnectionsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
AnimatedVisibility(
|
||||
visible = uiState.isSearchActive,
|
||||
enter = expandVertically() + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.searchText,
|
||||
onValueChange = { viewModel.setSearchText(it) },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 8.dp)
|
||||
.focusRequester(focusRequester),
|
||||
placeholder = { Text(stringResource(R.string.search_connections)) },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
trailingIcon = {
|
||||
if (uiState.searchText.isNotEmpty()) {
|
||||
IconButton(onClick = { viewModel.setSearchText("") }) {
|
||||
Icon(Icons.Default.Clear, contentDescription = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
|
||||
when {
|
||||
uiState.isLoading -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
uiState.connections.isEmpty() -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.empty_connections),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val bounceBlockingConnection = rememberBounceBlockingNestedScrollConnection(lazyListState)
|
||||
LazyColumn(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(bounceBlockingConnection),
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
overscrollEffect = rememberOverscrollEffect(),
|
||||
) {
|
||||
items(
|
||||
items = uiState.connections,
|
||||
key = { it.id },
|
||||
) { connection ->
|
||||
ConnectionItem(
|
||||
connection = connection,
|
||||
onClick = { onConnectionClick(connection) },
|
||||
onClose = { viewModel.closeConnection(connection.id) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberOverscrollEffect
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
@@ -49,6 +49,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
@@ -70,6 +71,7 @@ import io.nekohasekai.sfa.compose.model.Group
|
||||
import io.nekohasekai.sfa.compose.model.GroupItem
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||
import io.nekohasekai.sfa.compose.util.rememberSheetDismissFromContentOnlyIfGestureStartedAtTopModifier
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.utils.CommandClient
|
||||
|
||||
@@ -80,6 +82,8 @@ fun GroupsCard(
|
||||
commandClient: CommandClient? = null,
|
||||
viewModel: GroupsViewModel? = null,
|
||||
showTopBar: Boolean = false,
|
||||
listHeaderContent: (@Composable () -> Unit)? = null,
|
||||
asSheet: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val actualViewModel: GroupsViewModel = viewModel ?: viewModel(
|
||||
@@ -169,6 +173,8 @@ fun GroupsCard(
|
||||
onToggleExpanded = onToggleExpanded,
|
||||
onItemSelected = onItemSelected,
|
||||
onUrlTest = onUrlTest,
|
||||
listHeaderContent = listHeaderContent,
|
||||
asSheet = asSheet,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -179,51 +185,78 @@ private fun GroupsCardContent(
|
||||
onToggleExpanded: (String) -> Unit,
|
||||
onItemSelected: (String, String) -> Unit,
|
||||
onUrlTest: (String) -> Unit,
|
||||
listHeaderContent: (@Composable () -> Unit)? = null,
|
||||
asSheet: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
// Groups content
|
||||
if (uiState.isLoading) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else if (uiState.groups.isEmpty()) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "No groups available",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
val lazyListState = rememberSaveable(saver = LazyListState.Saver) { LazyListState() }
|
||||
val scrollModifier =
|
||||
if (asSheet) {
|
||||
rememberSheetDismissFromContentOnlyIfGestureStartedAtTopModifier {
|
||||
lazyListState.firstVisibleItemIndex == 0 &&
|
||||
lazyListState.firstVisibleItemScrollOffset == 0
|
||||
}
|
||||
} else {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val bounceBlockingConnection = rememberBounceBlockingNestedScrollConnection(lazyListState)
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(bounceBlockingConnection),
|
||||
state = lazyListState,
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 8.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Modifier.nestedScroll(rememberBounceBlockingNestedScrollConnection(lazyListState))
|
||||
}
|
||||
val overscrollEffect = if (asSheet) null else rememberOverscrollEffect()
|
||||
|
||||
LazyColumn(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.then(scrollModifier),
|
||||
state = lazyListState,
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 8.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
overscrollEffect = overscrollEffect,
|
||||
) {
|
||||
if (listHeaderContent != null) {
|
||||
item(key = "groups_list_header") {
|
||||
listHeaderContent()
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
uiState.isLoading -> {
|
||||
item(key = "groups_loading") {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiState.groups.isEmpty() -> {
|
||||
item(key = "groups_empty") {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "No groups available",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
items(
|
||||
items = uiState.groups,
|
||||
key = { it.tag },
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.nekohasekai.sfa.compose.util
|
||||
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
|
||||
@Composable
|
||||
fun rememberSheetDismissFromContentOnlyIfGestureStartedAtTopModifier(isAtTop: () -> Boolean): Modifier {
|
||||
val isAtTopState = rememberUpdatedState(isAtTop)
|
||||
val gestureStartedAtTop = remember { mutableStateOf(true) }
|
||||
|
||||
val nestedScrollConnection =
|
||||
remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||
if (source != NestedScrollSource.UserInput) return Offset.Zero
|
||||
val startedAtTop = gestureStartedAtTop.value
|
||||
return when {
|
||||
available.y < 0 -> available
|
||||
available.y > 0 && !startedAtTop -> available
|
||||
else -> Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
val startedAtTop = gestureStartedAtTop.value
|
||||
return when {
|
||||
available.y < 0 -> available
|
||||
available.y > 0 && !startedAtTop -> available
|
||||
else -> Velocity.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val gestureGateModifier =
|
||||
Modifier.pointerInput(Unit) {
|
||||
awaitEachGesture {
|
||||
awaitFirstDown(requireUnconsumed = false)
|
||||
gestureStartedAtTop.value = isAtTopState.value.invoke()
|
||||
do {
|
||||
val event = awaitPointerEvent()
|
||||
} while (event.changes.any { it.pressed })
|
||||
}
|
||||
}
|
||||
|
||||
return gestureGateModifier.nestedScroll(nestedScrollConnection)
|
||||
}
|
||||
Reference in New Issue
Block a user