Autocomplete tag input
This commit is contained in:
parent
5073cb0297
commit
060e2975aa
@ -30,6 +30,8 @@ interface CustomAttributesRepository {
|
|||||||
|
|
||||||
suspend fun export(toDir: File)
|
suspend fun export(toDir: File)
|
||||||
suspend fun import(fromDir: File)
|
suspend fun import(fromDir: File)
|
||||||
|
|
||||||
|
suspend fun getAllTags(startsWith: String? = null): List<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class CustomAttributesRepositoryImpl(
|
internal class CustomAttributesRepositoryImpl(
|
||||||
@ -103,6 +105,15 @@ internal class CustomAttributesRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getAllTags(startsWith: String?): List<String> {
|
||||||
|
val dao = appDatabase.customAttrsDao()
|
||||||
|
return if (startsWith != null) {
|
||||||
|
dao.getAllTagsLike("$startsWith%")
|
||||||
|
} else {
|
||||||
|
dao.getAllTags()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): Flow<List<Searchable>> {
|
override suspend fun search(query: String): Flow<List<Searchable>> {
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
return flow {
|
return flow {
|
||||||
|
|||||||
@ -32,4 +32,10 @@ interface CustomAttrsDao {
|
|||||||
clearCustomAttribute(key, "tag")
|
clearCustomAttribute(key, "tag")
|
||||||
insertCustomAttributes(tags)
|
insertCustomAttributes(tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Query("SELECT DISTINCT value FROM CustomAttributes WHERE type = 'tag' AND value LIKE :like ORDER BY value")
|
||||||
|
suspend fun getAllTagsLike(like: String): List<String>
|
||||||
|
|
||||||
|
@Query("SELECT DISTINCT value FROM CustomAttributes WHERE type = 'tag' ORDER BY value")
|
||||||
|
suspend fun getAllTags(): List<String>
|
||||||
}
|
}
|
||||||
@ -23,6 +23,7 @@ import androidx.compose.ui.input.key.onKeyEvent
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ fun OutlinedTagsInputField(
|
|||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
textStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
textStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
||||||
textColor: Color = LocalContentColor.current,
|
textColor: Color = LocalContentColor.current,
|
||||||
|
onAutocomplete: (suspend (query: String) -> List<String>)? = null
|
||||||
) {
|
) {
|
||||||
var value by remember { mutableStateOf("") }
|
var value by remember { mutableStateOf("") }
|
||||||
var lastTagFocused by remember { mutableStateOf(false) }
|
var lastTagFocused by remember { mutableStateOf(false) }
|
||||||
@ -42,6 +44,8 @@ fun OutlinedTagsInputField(
|
|||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var completions by remember(onAutocomplete) { mutableStateOf<List<String>>(emptyList()) }
|
||||||
|
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.onKeyEvent {
|
.onKeyEvent {
|
||||||
@ -73,6 +77,15 @@ fun OutlinedTagsInputField(
|
|||||||
onTagsChange(tags + newTags.dropLast(1).filter { it.isNotBlank() })
|
onTagsChange(tags + newTags.dropLast(1).filter { it.isNotBlank() })
|
||||||
}
|
}
|
||||||
value = newTags.last()
|
value = newTags.last()
|
||||||
|
if (value.isNotBlank()) {
|
||||||
|
onAutocomplete?.let {
|
||||||
|
scope.launch {
|
||||||
|
completions = it(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completions = emptyList()
|
||||||
|
}
|
||||||
lastTagFocused = false
|
lastTagFocused = false
|
||||||
},
|
},
|
||||||
textStyle = textStyle.copy(
|
textStyle = textStyle.copy(
|
||||||
@ -90,96 +103,81 @@ fun OutlinedTagsInputField(
|
|||||||
TextFieldDefaults.OutlinedTextFieldDecorationBox(
|
TextFieldDefaults.OutlinedTextFieldDecorationBox(
|
||||||
contentPadding = PaddingValues(0.dp),
|
contentPadding = PaddingValues(0.dp),
|
||||||
value = value,
|
value = value,
|
||||||
innerTextField = { Box {
|
innerTextField = {
|
||||||
Row(
|
Box {
|
||||||
modifier = Modifier.horizontalScroll(rememberScrollState()).padding(horizontal = 16.dp),
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
for ((i, tag) in tags.withIndex()) {
|
|
||||||
InputChip(
|
|
||||||
selected = i == tags.lastIndex && lastTagFocused,
|
|
||||||
modifier = Modifier.padding(end = 12.dp),
|
|
||||||
onClick = { },
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(imageVector = Icons.Rounded.Tag, contentDescription = null)
|
|
||||||
},
|
|
||||||
label = { Text(tag) },
|
|
||||||
trailingIcon = {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
onTagsChange(tags.filter { it != tag })
|
|
||||||
}, imageVector = Icons.Rounded.Clear, contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(56.dp),
|
.horizontalScroll(rememberScrollState())
|
||||||
contentAlignment = Alignment.CenterStart
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (value.isEmpty()) {
|
for ((i, tag) in tags.withIndex()) {
|
||||||
CompositionLocalProvider(
|
InputChip(
|
||||||
LocalTextStyle provides textStyle,
|
selected = i == tags.lastIndex && lastTagFocused,
|
||||||
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant,
|
modifier = Modifier.padding(end = 12.dp),
|
||||||
|
onClick = { },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Tag,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(tag) },
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onTagsChange(tags.filterIndexed { index, _ -> index != i })
|
||||||
|
},
|
||||||
|
imageVector = Icons.Rounded.Clear,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(56.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalTextStyle provides textStyle,
|
||||||
|
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
) {
|
||||||
|
placeholder?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (completions.isNotEmpty()) {
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = true,
|
||||||
|
onDismissRequest = { completions = emptyList() },
|
||||||
|
properties = PopupProperties(focusable = false)
|
||||||
) {
|
) {
|
||||||
placeholder?.invoke()
|
for (completion in completions) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(completion) },
|
||||||
|
onClick = {
|
||||||
|
onTagsChange(tags + completion)
|
||||||
|
value = ""
|
||||||
|
completions = emptyList()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
innerTextField()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
} },
|
|
||||||
enabled = true,
|
enabled = true,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
visualTransformation = VisualTransformation.None,
|
visualTransformation = VisualTransformation.None,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
)
|
)
|
||||||
/*Box {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.horizontalScroll(rememberScrollState()),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
for ((i, tag) in tags.withIndex()) {
|
|
||||||
InputChip(
|
|
||||||
selected = i == tags.lastIndex && lastTagFocused,
|
|
||||||
modifier = Modifier.padding(end = 12.dp),
|
|
||||||
onClick = { },
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(imageVector = Icons.Rounded.Tag, contentDescription = null)
|
|
||||||
},
|
|
||||||
label = { Text(tag) },
|
|
||||||
trailingIcon = {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
onTagsChange(tags.filter { it != tag })
|
|
||||||
}, imageVector = Icons.Rounded.Clear, contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 12.dp)
|
|
||||||
.padding(
|
|
||||||
start = if (tags.isEmpty()) 16.dp else 0.dp,
|
|
||||||
end = 16.dp
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (value.isEmpty()) {
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalTextStyle provides textStyle,
|
|
||||||
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
) {
|
|
||||||
placeholder?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}*/
|
|
||||||
},
|
},
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary)
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -124,11 +124,14 @@ fun CustomizeSearchableSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 16.dp),
|
.padding(vertical = 16.dp),
|
||||||
tags = tags, onTagsChange = { tags = it },
|
tags = tags, onTagsChange = { tags = it.distinct() },
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(stringResource(R.string.customize_tags_placeholder))
|
Text(stringResource(R.string.customize_tags_placeholder))
|
||||||
},
|
},
|
||||||
textStyle = MaterialTheme.typography.bodyMedium
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
onAutocomplete = {
|
||||||
|
viewModel.autocompleteTags(it).minus(tags.toSet())
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
DisposableEffect(searchable.key) {
|
DisposableEffect(searchable.key) {
|
||||||
|
|||||||
@ -84,4 +84,8 @@ class CustomizeSearchableSheetVM(
|
|||||||
fun getTags(): Flow<List<String>> {
|
fun getTags(): Flow<List<String>> {
|
||||||
return customAttributesRepository.getTags(searchable)
|
return customAttributesRepository.getTags(searchable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun autocompleteTags(query: String): List<String> {
|
||||||
|
return customAttributesRepository.getAllTags(startsWith = query)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user