From 3a1afc3c9f155680609a0b6775dd4169365365c5 Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 11 Jun 2024 15:36:05 +0200 Subject: [PATCH] feat(app): add search suggestion --- .../composeResources/files/enumbers.json | 74 ++++++------ .../src/commonMain/kotlin/component/Search.kt | 105 ++++++++++++++++++ .../commonMain/kotlin/tab/list/ENumberList.kt | 12 +- .../commonMain/kotlin/tab/list/ListMain.kt | 95 +++------------- 4 files changed, 164 insertions(+), 122 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/component/Search.kt diff --git a/composeApp/src/commonMain/composeResources/files/enumbers.json b/composeApp/src/commonMain/composeResources/files/enumbers.json index d971514..77cfb75 100644 --- a/composeApp/src/commonMain/composeResources/files/enumbers.json +++ b/composeApp/src/commonMain/composeResources/files/enumbers.json @@ -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 diff --git a/composeApp/src/commonMain/kotlin/component/Search.kt b/composeApp/src/commonMain/kotlin/component/Search.kt new file mode 100644 index 0000000..d58c4ab --- /dev/null +++ b/composeApp/src/commonMain/kotlin/component/Search.kt @@ -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) { + 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): List { + if (query.isEmpty()) { + return eNumbers + } + + val filtered = eNumbers.filter { eNumber -> + eNumber.number.contains(query, ignoreCase = true) + } + + return filtered +} diff --git a/composeApp/src/commonMain/kotlin/tab/list/ENumberList.kt b/composeApp/src/commonMain/kotlin/tab/list/ENumberList.kt index 0211376..e8a2342 100644 --- a/composeApp/src/commonMain/kotlin/tab/list/ENumberList.kt +++ b/composeApp/src/commonMain/kotlin/tab/list/ENumberList.kt @@ -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) : 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 diff --git a/composeApp/src/commonMain/kotlin/tab/list/ListMain.kt b/composeApp/src/commonMain/kotlin/tab/list/ListMain.kt index 6ca8ee7..1111268 100644 --- a/composeApp/src/commonMain/kotlin/tab/list/ListMain.kt +++ b/composeApp/src/commonMain/kotlin/tab/list/ListMain.kt @@ -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) - ) - } - } - } - } - } }