diff --git a/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt b/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt index 108d2a95..da716c4c 100644 --- a/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt +++ b/app/src/main/java/xyz/quaver/pupil/networking/SearchQuery.kt @@ -2,11 +2,24 @@ package xyz.quaver.pupil.networking import androidx.compose.ui.tooling.preview.PreviewParameterProvider +val validNamespace = listOf( + "female", + "male", + "artist", + "group", + "character", + "series", + "type", + "language", + "tag" +) + class SearchQueryPreviewParameterProvider: PreviewParameterProvider { override val values = sequenceOf( SearchQuery.And(listOf( SearchQuery.Or(listOf( SearchQuery.And(listOf( + SearchQuery.Tag("language", "thisisareallylongtagyoucantevenseetheendofthis"), SearchQuery.Tag("language", "korean"), SearchQuery.Tag("female", "unusual pupil"), SearchQuery.Tag("female", "collar") diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/QueryEditor.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/QueryEditor.kt index cded18b0..23e50af5 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/composable/QueryEditor.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/composable/QueryEditor.kt @@ -1,10 +1,15 @@ package xyz.quaver.pupil.ui.composable +import android.util.Log import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -13,21 +18,26 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.AddCircleOutline import androidx.compose.material.icons.filled.RemoveCircleOutline import androidx.compose.material3.Card import androidx.compose.material3.CardColors import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -35,18 +45,28 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.unit.dp import xyz.quaver.pupil.R import xyz.quaver.pupil.networking.SearchQuery +import xyz.quaver.pupil.networking.validNamespace import xyz.quaver.pupil.ui.theme.Blue300 import xyz.quaver.pupil.ui.theme.Blue600 import xyz.quaver.pupil.ui.theme.Gray300 import xyz.quaver.pupil.ui.theme.Pink600 import xyz.quaver.pupil.ui.theme.Red300 import xyz.quaver.pupil.ui.theme.Yellow400 +import kotlin.math.exp private fun SearchQuery.toEditableStateInternal(): EditableSearchQueryState = when (this) { is SearchQuery.Tag -> EditableSearchQueryState.Tag(namespace, tag) @@ -118,60 +138,129 @@ sealed interface EditableSearchQueryState { fun EditableTagChip( state: EditableSearchQueryState.Tag, isFavorite: Boolean = false, - enabled: Boolean = true, - leftIcon: @Composable (SearchQuery.Tag) -> Unit = { tag -> TagChipIcon(tag) }, - rightIcon: @Composable (SearchQuery.Tag) -> Unit = { _ -> Spacer(Modifier.width(16.dp)) }, - content: @Composable (SearchQuery.Tag) -> Unit = { tag -> Text(tag.tag.ifBlank { stringResource(R.string.search_bar_edit_tag) }) }, -) { - val namespace by state.namespace - val tag by state.tag - - val surfaceColor = if (isFavorite) Yellow400 else when (namespace) { - "male" -> Blue600 - "female" -> Pink600 - else -> MaterialTheme.colorScheme.surface + leftIcon: @Composable RowScope.(SearchQuery.Tag) -> Unit = { tag -> TagChipIcon(tag) }, + rightIcon: @Composable RowScope.(SearchQuery.Tag) -> Unit = { _ -> Spacer(Modifier.width(16.dp)) }, + content: @Composable RowScope.(SearchQuery.Tag) -> Unit = { tag -> + Text( + modifier = Modifier + .weight(1f, fill = false) + .horizontalScroll(rememberScrollState()), + text = tag.tag.ifBlank { stringResource(R.string.search_bar_edit_tag) } + ) } +) { + var namespace by state.namespace + var tag by state.tag - val contentColor = - if (surfaceColor == MaterialTheme.colorScheme.surface) - MaterialTheme.colorScheme.onSurface - else - Color.White + var expanded by remember { mutableStateOf(false) } - val inner = @Composable { - CompositionLocalProvider( - LocalContentColor provides contentColor, - LocalTextStyle provides MaterialTheme.typography.bodyMedium - ) { - val queryTag = SearchQuery.Tag(namespace, tag) + val surfaceColor by animateColorAsState( + when { + expanded -> MaterialTheme.colorScheme.surface + isFavorite -> Yellow400 + namespace == "male" -> Blue600 + namespace == "female" -> Pink600 + else -> MaterialTheme.colorScheme.surface + }, label = "tag surface color" + ) - Row( - verticalAlignment = Alignment.CenterVertically - ) { - leftIcon(queryTag) - content(queryTag) - rightIcon(queryTag) + val contentColor by animateColorAsState( + when { + expanded -> Color.White + isFavorite -> Color.White + namespace == "male" -> Color.White + namespace == "female" -> Color.White + else -> MaterialTheme.colorScheme.onSurface + }, label = "tag content color" + ) + + Surface( + shape = RoundedCornerShape(16.dp), + color = surfaceColor + ) { + AnimatedContent(targetState = expanded, label = "open tag editor") { targetExpanded -> + if (!targetExpanded) { + CompositionLocalProvider( + LocalContentColor provides contentColor, + LocalTextStyle provides MaterialTheme.typography.bodyMedium + ) { + val queryTag = SearchQuery.Tag(namespace, tag) + + Row( + modifier = Modifier + .height(32.dp) + .clickable { expanded = true }, + verticalAlignment = Alignment.CenterVertically + ) { + leftIcon(queryTag) + content(queryTag) + rightIcon(queryTag) + } + } + } else { + Column( + Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 8.dp, end = 8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = { + expanded = false + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "close tag editor" + ) + } + + var selection by remember { mutableStateOf(TextRange.Zero) } + var composition by remember { mutableStateOf(null) } + + val textFieldValue = remember(tag, selection, composition) { + TextFieldValue(tag, selection, composition) + } + + OutlinedTextField( + value = textFieldValue, + singleLine = true, + leadingIcon = { + TagChipIcon(SearchQuery.Tag(namespace, tag)) + }, + modifier = Modifier + .onKeyEvent { event -> + if (event.key == Key.Backspace && tag.isEmpty()) { + val newTag = namespace?.dropLast(1) ?: "" + namespace = null + tag = newTag + selection = TextRange(newTag.length) + composition = null + true + } else false + }, + keyboardActions = KeyboardActions( + onDone = { + expanded = false + } + ), + onValueChange = { newTextValue -> + val newTag = newTextValue.text.lowercase() + tag = if (namespace == null && newTag.trim() in validNamespace) { + namespace = newTag.trim() + "" + } else newTag + selection = newTextValue.selection + composition = newTextValue.composition + } + ) + } + } } } } - - val modifier = Modifier.height(32.dp) - val shape = RoundedCornerShape(16.dp) - - if (enabled) - Surface( - modifier = modifier, - shape = shape, - color = surfaceColor, - content = inner - ) - else - Surface( - modifier, - shape = shape, - color = surfaceColor, - content = inner - ) } @Composable @@ -267,7 +356,6 @@ fun QueryEditorQueryView( is EditableSearchQueryState.Tag -> { EditableTagChip( state, - enabled = false, rightIcon = { Icon( modifier = Modifier