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>
|
fun getLinksByMultipleTags(tag1: String, tag2: String): PagingSource<Int, LinkEntity>
|
||||||
|
|
||||||
|
@RawQuery(observedEntities = [LinkEntity::class])
|
||||||
|
fun getLinksByTags(query: SupportSQLiteQuery): PagingSource<Int, LinkEntity>
|
||||||
|
|
||||||
// ====== Filtres temporels ======
|
// ====== Filtres temporels ======
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@ -56,12 +57,22 @@ constructor(
|
|||||||
when {
|
when {
|
||||||
!searchTerm.isNullOrBlank() -> linkDao.searchLinks(searchTerm)
|
!searchTerm.isNullOrBlank() -> linkDao.searchLinks(searchTerm)
|
||||||
!searchTags.isNullOrBlank() -> {
|
!searchTags.isNullOrBlank() -> {
|
||||||
val tags = searchTags.split(" ").filter { it.isNotBlank() }
|
val tags =
|
||||||
if (tags.size == 1) {
|
searchTags
|
||||||
linkDao.getLinksByTag(tags.first())
|
.trim()
|
||||||
|
.split(Regex("\\s+"))
|
||||||
|
.map { it.trim() }
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.distinct()
|
||||||
|
|
||||||
|
if (tags.isEmpty()) {
|
||||||
|
linkDao.getAllLinksPaged()
|
||||||
} else {
|
} else {
|
||||||
// Pour plusieurs tags, on prend les liens qui ont au moins un des tags
|
val whereClause = tags.joinToString(" AND ") { "tags LIKE ?" }
|
||||||
linkDao.getLinksByTag(tags.first())
|
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()
|
else -> linkDao.getAllLinksPaged()
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
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.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
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.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.components.TagChip
|
||||||
import com.shaarit.ui.theme.*
|
import com.shaarit.ui.theme.*
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FeedScreen(
|
fun FeedScreen(
|
||||||
onNavigateToAdd: () -> Unit,
|
onNavigateToAdd: () -> Unit,
|
||||||
@ -241,27 +245,52 @@ fun FeedScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(DarkNavy)
|
.background(DarkNavy)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.Top,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Filtering by:",
|
"Filtering by:",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = TextSecondary
|
color = TextSecondary,
|
||||||
|
modifier = Modifier.padding(top = 6.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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(
|
TagChip(
|
||||||
tag = searchTags!!,
|
tag = tag,
|
||||||
isSelected = true,
|
isSelected = true,
|
||||||
onClick = { viewModel.clearTagFilter() }
|
onClick = { viewModel.onTagClicked(tag) }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
}
|
||||||
IconButton(onClick = { viewModel.clearTagFilter() }) {
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.clearTagFilter() },
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Close,
|
Icons.Default.Close,
|
||||||
contentDescription = "Clear filter",
|
contentDescription = "Clear filter",
|
||||||
tint = TextMuted,
|
tint = TextMuted,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(18.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user