feat(app): add search suggestion

This commit is contained in:
Anthony 2024-06-11 15:36:05 +02:00
parent 2880d57cc6
commit 3a1afc3c9f
4 changed files with 164 additions and 122 deletions

View File

@ -3,26 +3,26 @@
"group": 1,
"numbers": [
{
"number": "100",
"number": "E100",
"name": "Curcumin",
"vegan": true,
"notVegan": false
},
{
"number": "101",
"number": "E101",
"name": "Riboflavin",
"vegan": true,
"notVegan": true,
"note": "Can be isolated from milk. However, it is usually derived from micro-organisms as it is cheaper."
},
{
"number": "101a",
"number": "E101a",
"name": "Riboflavin-5'-phosphate",
"vegan": true,
"notVegan": false
},
{
"number": "102",
"number": "E102",
"name": "Tartrazine",
"vegan": true,
"notVegan": false
@ -33,19 +33,19 @@
"group": 2,
"numbers": [
{
"number": "200",
"number": "E200",
"name": "Sorbic acid",
"vegan": true,
"notVegan": false
},
{
"number": "201",
"number": "E201",
"name": "Sodium sorbate",
"vegan": true,
"notVegan": false
},
{
"number": "202",
"number": "E202",
"name": "Potassium sorbate",
"vegan": true,
"notVegan": false
@ -56,19 +56,19 @@
"group": 3,
"numbers": [
{
"number": "300",
"number": "E300",
"name": "Ascorbic acid",
"vegan": true,
"notVegan": false
},
{
"number": "301",
"number": "E301",
"name": "Sodium ascorbate",
"vegan": true,
"notVegan": false
},
{
"number": "302",
"number": "E302",
"name": "Calcium ascorbate",
"vegan": true,
"notVegan": false
@ -79,19 +79,19 @@
"group": 4,
"numbers": [
{
"number": "400",
"number": "E400",
"name": "Alginic acid",
"vegan": true,
"notVegan": false
},
{
"number": "401",
"number": "E401",
"name": "Sodium alginate",
"vegan": true,
"notVegan": false
},
{
"number": "402",
"number": "E402",
"name": "Potassium alginate",
"vegan": true,
"notVegan": false
@ -102,19 +102,19 @@
"group": 5,
"numbers": [
{
"number": "500",
"number": "E500",
"name": "Sodium carbonates",
"vegan": true,
"notVegan": false
},
{
"number": "501",
"number": "E501",
"name": "Potassium carbonates",
"vegan": true,
"notVegan": false
},
{
"number": "503",
"number": "E503",
"name": "Ammonium carbonates",
"vegan": true,
"notVegan": false
@ -125,21 +125,21 @@
"group": 6,
"numbers": [
{
"number": "620",
"number": "E620",
"name": "Glutamic acid",
"vegan": true,
"notVegan": false,
"note": "Commercially only made from sugar by bacterial fermentation, or from seaweed. In theory could be made from any protein, but it would be too expensive."
},
{
"number": "621",
"number": "E621",
"name": "Monosodium glutamate",
"vegan": true,
"notVegan": false,
"note": "Commercially only made from sugar by bacterial fermentation, or from seaweed. In theory could be made from any protein, but it would be too expensive."
},
{
"number": "622",
"number": "E622",
"name": "Monopotassium glutamate",
"vegan": true,
"notVegan": false,
@ -151,19 +151,19 @@
"group": 7,
"numbers": [
{
"number": "701",
"number": "E701",
"name": "Tetracyclines",
"vegan": true,
"notVegan": false
},
{
"number": "702",
"number": "E702",
"name": "Chlortetracycline",
"vegan": true,
"notVegan": false
},
{
"number": "703",
"number": "E703",
"name": "Oxytetracycline",
"vegan": true,
"notVegan": false
@ -174,37 +174,37 @@
"group": 9,
"numbers": [
{
"number": "900",
"number": "E900",
"name": "Dimethylpolysiloxane",
"vegan": true,
"notVegan": false
},
{
"number": "901",
"number": "E901",
"name": "Beeswax",
"vegan": false,
"notVegan": true
},
{
"number": "902",
"number": "E902",
"name": "Candelilla wax",
"vegan": true,
"notVegan": false
},
{
"number": "903",
"number": "E903",
"name": "Carnauba wax",
"vegan": true,
"notVegan": false
},
{
"number": "904",
"number": "E904",
"name": "Shellac",
"vegan": false,
"notVegan": true
},
{
"number": "905",
"number": "E905",
"name": "Microcrystalline wax",
"vegan": true,
"notVegan": false
@ -215,57 +215,57 @@
"group": 10,
"numbers": [
{
"number": "1000",
"number": "E1000",
"name": "Cholic acid",
"vegan": false,
"notVegan": true,
"note": "Derived from beef (bile)"
},
{
"number": "1001",
"number": "E1001",
"name": "Choline salts",
"vegan": true,
"notVegan": false
},
{
"number": "1100",
"number": "E1100",
"name": "Amylase",
"vegan": true,
"notVegan": false
},
{
"number": "1101",
"number": "E1101",
"name": "Proteases",
"vegan": true,
"notVegan": false
},
{
"number": "1102",
"number": "E1102",
"name": "Glucose oxidase",
"vegan": true,
"notVegan": false
},
{
"number": "1103",
"number": "E1103",
"name": "Invertase",
"vegan": true,
"notVegan": false
},
{
"number": "1104",
"number": "E1104",
"name": "Lipases",
"vegan": true,
"notVegan": false
},
{
"number": "1105",
"number": "E1105",
"name": "Lysozyme",
"vegan": false,
"notVegan": true,
"note": "Derived from chicken eggs"
},
{
"number": "1200",
"number": "E1200",
"name": "Polydextrose",
"vegan": true,
"notVegan": false

View File

@ -0,0 +1,105 @@
package component
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
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.Clear
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.unit.dp
import data.ENumber
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Search(eNumbers: List<ENumber>) {
var text by rememberSaveable { mutableStateOf("") }
var expanded by rememberSaveable { mutableStateOf(false) }
Column (
Modifier
.fillMaxWidth()
.padding(if (!expanded) 8.dp else 0.dp)
.semantics { isTraversalGroup = true }
) {
SearchBar(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.semantics { traversalIndex = 0f },
query = text,
onQueryChange = { text = it },
onSearch = { expanded = false },
onActiveChange = { expanded = it },
active = expanded,
placeholder = { Text("Search E Numbers") },
leadingIcon = {
IconButton(onClick = {
if (expanded) {
expanded = false
} else {
/* TODO add navigation menu functionality */
}
}) {
AnimatedContent(
targetState = expanded,
) { targetExpanded ->
if (targetExpanded) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Exit Search")
} else {
Icon(Icons.Filled.Menu, "Navigation Menu")
}
}
}
},
trailingIcon = {
if (text.isNotBlank()) {
IconButton(onClick = { text = "" }) {
Icon(imageVector = Icons.Filled.Clear, contentDescription = "Clear Search")
}
}
}
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
searchSuggestion(query = text, eNumbers = eNumbers).forEach {
ListItem(
headlineContent = { Text(text = it.number) },
modifier = Modifier
.clickable {
text = it.number
expanded = false
}
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
)
}
}
}
}
}
private fun searchSuggestion(query: String, eNumbers: List<ENumber>): List<ENumber> {
if (query.isEmpty()) {
return eNumbers
}
val filtered = eNumbers.filter { eNumber ->
eNumber.number.contains(query, ignoreCase = true)
}
return filtered
}

View File

@ -12,7 +12,10 @@ import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.*
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@ -58,17 +61,14 @@ class ENumberList(private val eNumbers: List<EGroup>) : Screen {
@Composable
private fun ItemList(eNumber: ENumber) {
ListItem(
headlineContent = {
Text(text = "E${eNumber.number}")
},
headlineContent = { Text(text = eNumber.number) },
leadingContent = {
VeganStatusIcon(
vegan = eNumber.vegan,
notVegan = eNumber.notVegan
)
}
},
)
HorizontalDivider()
}
@Composable

View File

@ -1,28 +1,20 @@
package tab.list
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
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.*
import androidx.compose.material3.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import component.Search
import tab.LoadingScreen
object ListMain : Tab {
@ -50,7 +42,15 @@ object ListMain : Tab {
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { Search() }
topBar = {
when (val s = state) {
is ListScreenModel.State.Result -> {
val ungroupedENumbers = s.eNumbers.flatMap { it.numbers }
Search(ungroupedENumbers)
}
else -> Unit
}
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
@ -66,67 +66,4 @@ object ListMain : Tab {
screenModel.getENumbers()
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Search() {
var text by rememberSaveable { mutableStateOf("") }
var expanded by rememberSaveable { mutableStateOf(false) }
Column (
Modifier
.fillMaxWidth()
.padding(if (!expanded) 8.dp else 0.dp)
.semantics { isTraversalGroup = true }
) {
SearchBar(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.semantics { traversalIndex = 0f },
query = text,
onQueryChange = { text = it },
onSearch = { expanded = false },
onActiveChange = { expanded = it },
active = expanded,
placeholder = {Text("Search E Numbers")},
leadingIcon = {
if (expanded) {
IconButton(onClick = { expanded = false }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Exit Search")
}
} else {
IconButton(onClick = { /* TODO Add menu */ }) {
Icon(Icons.Filled.Menu, "Navigation Menu")
}
}
},
trailingIcon = {
if (text.isNotBlank()) {
IconButton(onClick = { text = "" }) {
Icon(imageVector = Icons.Filled.Clear, contentDescription = "Clear Search")
}
}
}
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
repeat(4) { idx ->
val resultText = "Suggestion $idx"
ListItem(
headlineContent = { Text(resultText) },
supportingContent = { Text("Additional info") },
leadingContent = { Icon(Icons.Filled.Star, contentDescription = null) },
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
modifier = Modifier
.clickable {
text = resultText
expanded = false
}
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
)
}
}
}
}
}
}