mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
feat: add empty state, filter, and overlay animations in feed
This commit is contained in:
parent
64662f6bd4
commit
71d58008c6
1 changed files with 161 additions and 94 deletions
|
|
@ -9,6 +9,10 @@ import androidx.compose.animation.animateColorAsState
|
|||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import kotlinx.coroutines.delay
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
|
|
@ -287,7 +291,11 @@ fun FeedScreen(
|
|||
.padding(padding)
|
||||
) {
|
||||
// Filter chips bar (only when not searching)
|
||||
if (!isSearchActive) {
|
||||
AnimatedVisibility(
|
||||
visible = !isSearchActive,
|
||||
enter = fadeIn(SwooshMotion.quick()) + expandVertically(),
|
||||
exit = fadeOut(SwooshMotion.quick()) + shrinkVertically()
|
||||
) {
|
||||
FilterChipsBar(
|
||||
activeFilter = activeFilter,
|
||||
onFilterSelected = { viewModel.setFilter(it) }
|
||||
|
|
@ -312,7 +320,11 @@ fun FeedScreen(
|
|||
}
|
||||
|
||||
// Loading overlay during account switch
|
||||
if (state.isSwitchingAccount) {
|
||||
AnimatedVisibility(
|
||||
visible = state.isSwitchingAccount,
|
||||
enter = fadeIn(SwooshMotion.quick()),
|
||||
exit = fadeOut(SwooshMotion.quick())
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
|
|
@ -327,7 +339,8 @@ fun FeedScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
} else if (isSearchActive && searchQuery.isBlank() && recentSearches.isNotEmpty()) {
|
||||
}
|
||||
if (!state.isSwitchingAccount && isSearchActive && searchQuery.isBlank() && recentSearches.isNotEmpty()) {
|
||||
// Show recent searches when search is active but query is empty
|
||||
RecentSearchesList(
|
||||
recentSearches = recentSearches,
|
||||
|
|
@ -336,6 +349,13 @@ fun FeedScreen(
|
|||
)
|
||||
} else if (isSearchActive && searchQuery.isNotBlank() && isSearching) {
|
||||
// Searching indicator
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = fadeIn(SwooshMotion.quick()) + scaleIn(
|
||||
initialScale = 0.9f,
|
||||
animationSpec = SwooshMotion.quick()
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
|
|
@ -350,8 +370,16 @@ fun FeedScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isSearchActive && searchQuery.isNotBlank() && searchResults.isEmpty() && !isSearching) {
|
||||
// No results empty state
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = fadeIn(SwooshMotion.quick()) + scaleIn(
|
||||
initialScale = 0.9f,
|
||||
animationSpec = SwooshMotion.quick()
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
|
|
@ -381,6 +409,7 @@ fun FeedScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!isSearchActive && displayPosts.isEmpty() && !state.isRefreshing) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -390,9 +419,19 @@ fun FeedScreen(
|
|||
if (state.isConnectionError && state.error != null) {
|
||||
// Connection error empty state
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 32.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = fadeIn(SwooshMotion.quick()) + scaleIn(
|
||||
initialScale = 0.9f,
|
||||
animationSpec = SwooshMotion.quick()
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
|
|
@ -420,12 +459,24 @@ fun FeedScreen(
|
|||
Text("Retry")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Filter-aware empty state
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = fadeIn(SwooshMotion.quick()) + scaleIn(
|
||||
initialScale = 0.9f,
|
||||
animationSpec = SwooshMotion.quick()
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = activeFilter.emptyMessage(),
|
||||
|
|
@ -448,6 +499,8 @@ fun FeedScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = state.isRefreshing,
|
||||
|
|
@ -495,6 +548,16 @@ fun FeedScreen(
|
|||
// Normal feed: pinned section + swipe actions
|
||||
// Pinned posts (no section header — pin icon on post)
|
||||
if (pinnedPosts.isNotEmpty()) {
|
||||
item(key = "pinned_header") {
|
||||
Column {
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = fadeIn(SwooshMotion.quick()) + slideInVertically(initialOffsetY = { -it / 2 })
|
||||
) {
|
||||
PinnedSectionHeader()
|
||||
}
|
||||
}
|
||||
}
|
||||
items(pinnedPosts, key = { "pinned_${it.ghostId ?: "local_${it.localId}"}" }) { post ->
|
||||
val itemKey = post.ghostId ?: "local_${post.localId}"
|
||||
StaggeredItem(
|
||||
|
|
@ -921,7 +984,6 @@ fun SwipeablePostCard(
|
|||
SwipeBackground(dismissState)
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.semantics {
|
||||
customActions = listOf(
|
||||
CustomAccessibilityAction("Edit post") {
|
||||
|
|
@ -934,6 +996,10 @@ fun SwipeablePostCard(
|
|||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
PostCardContent(
|
||||
post = post,
|
||||
|
|
@ -949,6 +1015,7 @@ fun SwipeablePostCard(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -985,7 +1052,7 @@ fun SwipeBackground(dismissState: SwipeToDismissBoxState) {
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color, MaterialTheme.shapes.medium)
|
||||
.background(color)
|
||||
.padding(horizontal = 24.dp),
|
||||
contentAlignment = alignment
|
||||
) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue