feat: [#24] recherche globale (debounce 300ms, min 2 chars, live results)
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
package com.planify.mobile.ui.search
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Close
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.planify.mobile.domain.model.Task
|
||||||
|
import com.planify.mobile.ui.components.TaskRow
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchScreen(
|
||||||
|
onTaskClick: (Task) -> Unit,
|
||||||
|
viewModel: SearchViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val query by viewModel.query.collectAsState()
|
||||||
|
val results by viewModel.results.collectAsState()
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = query,
|
||||||
|
onValueChange = viewModel::setQuery,
|
||||||
|
placeholder = { Text("Rechercher des tâches…") },
|
||||||
|
leadingIcon = { Icon(Icons.Outlined.Search, contentDescription = null) },
|
||||||
|
trailingIcon = {
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
IconButton(onClick = { viewModel.setQuery("") }) {
|
||||||
|
Icon(Icons.Outlined.Close, contentDescription = "Effacer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
when {
|
||||||
|
query.length < 2 -> Text(
|
||||||
|
text = "Saisissez au moins 2 caractères",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
results.isEmpty() -> Text(
|
||||||
|
text = "Aucun résultat pour « $query »",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
else -> LazyColumn {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "${results.size} résultat(s)",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(results, key = { it.id }) { task ->
|
||||||
|
TaskRow(
|
||||||
|
task = task,
|
||||||
|
onClick = { onTaskClick(task) },
|
||||||
|
onCheckedChange = { viewModel.toggleTask(task) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.planify.mobile.ui.search
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.planify.mobile.domain.model.Task
|
||||||
|
import com.planify.mobile.domain.repository.TaskRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SearchViewModel @Inject constructor(
|
||||||
|
private val taskRepository: TaskRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val query = MutableStateFlow("")
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||||
|
val results = query
|
||||||
|
.debounce(300)
|
||||||
|
.flatMapLatest { q ->
|
||||||
|
if (q.length < 2) flowOf(emptyList())
|
||||||
|
else taskRepository.searchTasks(q)
|
||||||
|
}
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||||
|
|
||||||
|
fun setQuery(q: String) { query.value = q }
|
||||||
|
|
||||||
|
fun toggleTask(task: Task) {
|
||||||
|
viewModelScope.launch { taskRepository.checkTask(task.id, !task.checked) }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user