feat: Implement multi-tag filtering with dynamic SQL queries and improved UI
- Add RawQuery method getLinksByTags() to LinkDao for flexible tag filtering - Implement dynamic SQL query generation for AND-based multi-tag filtering in LinkRepositoryImpl - Parse and normalize tag input with whitespace handling and deduplication - Replace single tag chip with FlowRow layout to display multiple selected tags - Add individual tag removal capability while maintaining filter state - Improve tag filter banner
This commit is contained in:
parent
7277342d4a
commit
fdacf2248a
@ -68,6 +68,9 @@ interface LinkDao {
|
||||
""")
|
||||
fun getLinksByMultipleTags(tag1: String, tag2: String): PagingSource<Int, LinkEntity>
|
||||
|
||||
@RawQuery(observedEntities = [LinkEntity::class])
|
||||
fun getLinksByTags(query: SupportSQLiteQuery): PagingSource<Int, LinkEntity>
|
||||
|
||||
// ====== Filtres temporels ======
|
||||
|
||||
@Query("""
|
||||
|
||||
@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import retrofit2.HttpException
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ -56,12 +57,22 @@ constructor(
|
||||
when {
|
||||
!searchTerm.isNullOrBlank() -> linkDao.searchLinks(searchTerm)
|
||||
!searchTags.isNullOrBlank() -> {
|
||||
val tags = searchTags.split(" ").filter { it.isNotBlank() }
|
||||
if (tags.size == 1) {
|
||||
linkDao.getLinksByTag(tags.first())
|
||||
val tags =
|
||||
searchTags
|
||||
.trim()
|
||||
.split(Regex("\\s+"))
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.distinct()
|
||||
|
||||
if (tags.isEmpty()) {
|
||||
linkDao.getAllLinksPaged()
|
||||
} else {
|
||||
// Pour plusieurs tags, on prend les liens qui ont au moins un des tags
|
||||
linkDao.getLinksByTag(tags.first())
|
||||
val whereClause = tags.joinToString(" AND ") { "tags LIKE ?" }
|
||||
val sql =
|
||||
"SELECT * FROM links WHERE $whereClause ORDER BY is_pinned DESC, created_at DESC"
|
||||
val args: Array<Any> = tags.map { "%\"$it\"%" }.toTypedArray()
|
||||
linkDao.getLinksByTags(SimpleSQLiteQuery(sql, args))
|
||||
}
|
||||
}
|
||||
else -> linkDao.getAllLinksPaged()
|
||||
|
||||
@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
@ -15,6 +17,8 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -31,7 +35,7 @@ import com.shaarit.ui.components.PremiumTextField
|
||||
import com.shaarit.ui.components.TagChip
|
||||
import com.shaarit.ui.theme.*
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun FeedScreen(
|
||||
onNavigateToAdd: () -> Unit,
|
||||
@ -241,27 +245,52 @@ fun FeedScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(DarkNavy)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
"Filtering by:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = TextSecondary
|
||||
color = TextSecondary,
|
||||
modifier = Modifier.padding(top = 6.dp)
|
||||
)
|
||||
TagChip(
|
||||
tag = searchTags!!,
|
||||
isSelected = true,
|
||||
onClick = { viewModel.clearTagFilter() }
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = { viewModel.clearTagFilter() }) {
|
||||
|
||||
val selectedTags =
|
||||
remember(searchTags) {
|
||||
searchTags
|
||||
.orEmpty()
|
||||
.trim()
|
||||
.split(Regex("\\s+"))
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.distinct()
|
||||
}
|
||||
|
||||
FlowRow(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
maxItemsInEachRow = 3
|
||||
) {
|
||||
selectedTags.forEach { tag ->
|
||||
TagChip(
|
||||
tag = tag,
|
||||
isSelected = true,
|
||||
onClick = { viewModel.onTagClicked(tag) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { viewModel.clearTagFilter() },
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = "Clear filter",
|
||||
tint = TextMuted,
|
||||
modifier = Modifier.size(20.dp)
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user