mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 11:55:47 +00:00
feat: add tag filter chips in Feed with popular tags LazyRow
FeedViewModel fetches tags on refresh(), takes top 10 by post count. FeedScreen shows LazyRow of FilterChip below status filter: "All tags" first, then popular tags with post counts. Tapping filters posts by tag. Post cards now show tags in compact labelSmall format joined by dots.
This commit is contained in:
parent
11b20fd42a
commit
aaf29f1512
2 changed files with 95 additions and 29 deletions
|
|
@ -33,6 +33,7 @@ import androidx.compose.foundation.horizontalScroll
|
|||
import androidx.compose.foundation.layout.*
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
|
|
@ -83,6 +84,7 @@ import com.swoosh.microblog.data.CredentialsManager
|
|||
import com.swoosh.microblog.data.ShareUtils
|
||||
import com.swoosh.microblog.data.model.FeedPost
|
||||
import com.swoosh.microblog.data.model.GhostAccount
|
||||
import com.swoosh.microblog.data.model.GhostTagFull
|
||||
import com.swoosh.microblog.data.model.PostFilter
|
||||
import com.swoosh.microblog.data.model.PostStats
|
||||
import com.swoosh.microblog.data.model.QueueStatus
|
||||
|
|
@ -109,6 +111,7 @@ fun FeedScreen(
|
|||
val recentSearches by viewModel.recentSearches.collectAsStateWithLifecycle()
|
||||
val accounts by viewModel.accounts.collectAsStateWithLifecycle()
|
||||
val activeAccount by viewModel.activeAccount.collectAsStateWithLifecycle()
|
||||
val popularTags by viewModel.popularTags.collectAsStateWithLifecycle()
|
||||
val listState = rememberLazyListState()
|
||||
val context = LocalContext.current
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
|
@ -305,20 +308,17 @@ fun FeedScreen(
|
|||
)
|
||||
}
|
||||
|
||||
// Active tag filter bar
|
||||
if (state.activeTagFilter != null) {
|
||||
FilterChip(
|
||||
onClick = { viewModel.clearTagFilter() },
|
||||
label = { Text("#${state.activeTagFilter}") },
|
||||
selected = true,
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Tag, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(Icons.Default.Close, contentDescription = "Clear filter", modifier = Modifier.size(16.dp))
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
// Tag filter chips
|
||||
AnimatedVisibility(
|
||||
visible = !isSearchActive && popularTags.isNotEmpty(),
|
||||
enter = fadeIn(SwooshMotion.quick()) + expandVertically(),
|
||||
exit = fadeOut(SwooshMotion.quick()) + shrinkVertically()
|
||||
) {
|
||||
TagFilterChipsBar(
|
||||
tags = popularTags,
|
||||
activeTagFilter = state.activeTagFilter,
|
||||
onTagSelected = { viewModel.filterByTag(it) },
|
||||
onClearFilter = { viewModel.clearTagFilter() }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -775,6 +775,58 @@ fun FilterChipsBar(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TagFilterChipsBar(
|
||||
tags: List<GhostTagFull>,
|
||||
activeTagFilter: String?,
|
||||
onTagSelected: (String) -> Unit,
|
||||
onClearFilter: () -> Unit
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// "All tags" chip
|
||||
item {
|
||||
val isAllSelected = activeTagFilter == null
|
||||
FilterChip(
|
||||
selected = isAllSelected,
|
||||
onClick = { onClearFilter() },
|
||||
label = { Text("All tags") },
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Tag chips
|
||||
items(tags, key = { it.id ?: it.name }) { tag ->
|
||||
val isSelected = activeTagFilter != null && activeTagFilter.equals(tag.name, ignoreCase = true)
|
||||
FilterChip(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (isSelected) onClearFilter() else onTagSelected(tag.name)
|
||||
},
|
||||
label = {
|
||||
val postCount = tag.count?.posts
|
||||
if (postCount != null) {
|
||||
Text("${tag.name} ($postCount)")
|
||||
} else {
|
||||
Text(tag.name)
|
||||
}
|
||||
},
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SortButton(
|
||||
currentSort: SortOrder,
|
||||
|
|
@ -1519,24 +1571,20 @@ fun PostCardContent(
|
|||
}
|
||||
}
|
||||
|
||||
// Hashtag tags (bold colored text, not chips)
|
||||
// Tags display
|
||||
if (post.tags.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
post.tags.forEach { tag ->
|
||||
Text(
|
||||
text = "#$tag",
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
|
||||
text = post.tags.joinToString(" \u00B7 ") { "#$it" },
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.clickable { onTagClick(tag) }
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.clickable {
|
||||
post.tags.firstOrNull()?.let { onTagClick(it) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Queue status
|
||||
if (post.queueStatus != QueueStatus.NONE) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.swoosh.microblog.data.api.ApiClient
|
|||
import com.swoosh.microblog.data.db.Converters
|
||||
import com.swoosh.microblog.data.model.*
|
||||
import com.swoosh.microblog.data.repository.PostRepository
|
||||
import com.swoosh.microblog.data.repository.TagRepository
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
|
|
@ -38,6 +39,7 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
|||
|
||||
private val accountManager = AccountManager(application)
|
||||
private var repository = PostRepository(application)
|
||||
private var tagRepository = TagRepository(application)
|
||||
private val feedPreferences = FeedPreferences(application)
|
||||
private val searchHistoryManager = SearchHistoryManager(application)
|
||||
|
||||
|
|
@ -71,6 +73,9 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
|||
private val _recentSearches = MutableStateFlow<List<String>>(emptyList())
|
||||
val recentSearches: StateFlow<List<String>> = _recentSearches.asStateFlow()
|
||||
|
||||
private val _popularTags = MutableStateFlow<List<GhostTagFull>>(emptyList())
|
||||
val popularTags: StateFlow<List<GhostTagFull>> = _popularTags.asStateFlow()
|
||||
|
||||
private val _accounts = MutableStateFlow<List<GhostAccount>>(emptyList())
|
||||
val accounts: StateFlow<List<GhostAccount>> = _accounts.asStateFlow()
|
||||
|
||||
|
|
@ -237,8 +242,9 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
|||
accountManager.setActiveAccount(accountId)
|
||||
ApiClient.reset()
|
||||
|
||||
// Re-create repository to pick up new account
|
||||
// Re-create repositories to pick up new account
|
||||
repository = PostRepository(getApplication())
|
||||
tagRepository = TagRepository(getApplication())
|
||||
|
||||
refreshAccountsList()
|
||||
|
||||
|
|
@ -296,6 +302,18 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
|||
val sort = _sortOrder.value
|
||||
val tagFilter = _uiState.value.activeTagFilter
|
||||
|
||||
// Fetch popular tags in parallel
|
||||
launch {
|
||||
tagRepository.fetchTags().fold(
|
||||
onSuccess = { tags ->
|
||||
_popularTags.value = tags
|
||||
.sortedByDescending { it.count?.posts ?: 0 }
|
||||
.take(10)
|
||||
},
|
||||
onFailure = { /* silently ignore tag fetch failures */ }
|
||||
)
|
||||
}
|
||||
|
||||
repository.fetchPosts(page = 1, filter = filter, sortOrder = sort, tagFilter = tagFilter).fold(
|
||||
onSuccess = { response ->
|
||||
remotePosts = response.posts.map { it.toFeedPost() }
|
||||
|
|
|
|||
Loading…
Reference in a new issue