diff --git a/app/src/main/java/xyz/pixelatedw/recipe/data/RecipesView.kt b/app/src/main/java/xyz/pixelatedw/recipe/data/RecipesView.kt index eeb4d3c..3aead96 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/data/RecipesView.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/data/RecipesView.kt @@ -12,18 +12,35 @@ class RecipesView : ViewModel() { private val _recipes = MutableStateFlow>( arrayListOf() ) val recipes = _recipes.asStateFlow() + private val _tagFilters = MutableStateFlow>( arrayListOf() ) + val tagFilters = _tagFilters.asStateFlow() + private val _search = MutableStateFlow(null) val search = _search.asStateFlow() private val _keepScreenOn = MutableStateFlow(false) val keepScreenOn = _keepScreenOn.asStateFlow() + fun setTagFilterState(tag: String, state: Boolean) { + _tagFilters.value = _tagFilters.value.toMutableList().apply { replaceAll { it -> if (tag == it.tag) it.checked = state; it } }.toList() + } + fun setKeepScreenOn(flag: Boolean) { _keepScreenOn.update { flag } } fun setRecipes(recipes: List) { _recipes.update { recipes } + + val filters = arrayListOf() + + _recipes.value.stream() + .filter { it.tags.isNotEmpty() } + .forEach { + it.tags.forEach {tag -> filters.add(TagFilter(tag.name))} + } + + _tagFilters.update { filters.distinct() } } fun removeRecipe(recipe: RecipeWithTags) { diff --git a/app/src/main/java/xyz/pixelatedw/recipe/data/TagFilter.kt b/app/src/main/java/xyz/pixelatedw/recipe/data/TagFilter.kt new file mode 100644 index 0000000..abf3104 --- /dev/null +++ b/app/src/main/java/xyz/pixelatedw/recipe/data/TagFilter.kt @@ -0,0 +1,4 @@ +package xyz.pixelatedw.recipe.data + +data class TagFilter(val tag: String, var checked: Boolean = false) { +} diff --git a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipeDeletionDialog.kt b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/DeleteRecipeDialog.kt similarity index 90% rename from app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipeDeletionDialog.kt rename to app/src/main/java/xyz/pixelatedw/recipe/ui/components/DeleteRecipeDialog.kt index 49cef0b..f518cad 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipeDeletionDialog.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/DeleteRecipeDialog.kt @@ -3,13 +3,11 @@ package xyz.pixelatedw.recipe.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -21,7 +19,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @Composable -fun RecipeDeletionDialog(onAccept: ( ) -> Unit, onDismiss: () -> Unit) { +fun DeleteRecipeDialog(onAccept: ( ) -> Unit, onDismiss: () -> Unit) { Dialog(onDismissRequest = { onDismiss() }) { Card( modifier = Modifier diff --git a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/MainScreen.kt b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/MainScreen.kt index 5a81b5c..ed76c64 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/MainScreen.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/MainScreen.kt @@ -9,15 +9,19 @@ 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.material3.Button +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -25,24 +29,48 @@ import androidx.navigation.compose.rememberNavController import xyz.pixelatedw.recipe.MainActivity import xyz.pixelatedw.recipe.data.RecipeWithTags import xyz.pixelatedw.recipe.data.RecipesView +import xyz.pixelatedw.recipe.data.TagFilter @Composable fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) { val recipes = view.recipes.collectAsState() val active = view.activeRecipe.collectAsState() val search = view.search.collectAsState() + val filters = view.tagFilters.collectAsState() val navController = rememberNavController() val isInSearch = isInSearch@{ entry: RecipeWithTags -> - val hasTitle = entry.recipe.title.contains(search.value.orEmpty(), ignoreCase = true) + val hasTitle = search.value != null && search.value!!.isNotEmpty() && entry.recipe.title.contains(search.value.orEmpty(), ignoreCase = true) val hasTags = entry.tags.isNotEmpty() && entry.tags.stream() .filter { tag -> tag.name.contains(search.value.orEmpty(), ignoreCase = true) } .count() > 0 + var hasAtLeastOneFilter = false + var hasTagFilters = false + + for (filter in filters.value) { + if (filter.checked) { + hasAtLeastOneFilter = true + hasTagFilters = entry.tags.isNotEmpty() && entry.tags.stream() + .filter { tag -> tag.name.contains(filter.tag, ignoreCase = true) } + .count() > 0 + + if(hasTagFilters) { + break + } + } + } + + // TODO Needs much better filtering + if (hasAtLeastOneFilter) { + return@isInSearch hasTagFilters + } hasTitle || hasTags } + val openTagFilterDialog = remember { mutableStateOf(false) } + NavHost( navController = navController, startDestination = "list" @@ -54,19 +82,31 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) { .fillMaxWidth() .padding(8.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { OutlinedTextField( + modifier = Modifier.weight(0.7f), value = search.value.orEmpty(), onValueChange = { search -> view.setSearch(search) }, ) - Button( + // Tags + IconButton( + modifier = Modifier.weight(0.15f), + onClick = { openTagFilterDialog.value = true }, + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "Toggle tags filtering menu" + ) + } + // Load + IconButton( + modifier = Modifier.weight(0.15f), onClick = { ctx.sourceChooser.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) }, ) { - Text( - text = "Load", - maxLines = 1, - textAlign = TextAlign.Center, + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Load recipes from filesystem" ) } } @@ -82,6 +122,19 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) { } } } + + when { + openTagFilterDialog.value -> { + TagFilterDialog( + onAccept = { + // TODO This calls twice and therefore updates the search twice too, also hella stupid updating logic + openTagFilterDialog.value = false + view.setTagFilterState("", true) + }, + view, + ) + } + } } composable("info") { RecipeInfo(ctx, view, navController, padding, active.value!!) diff --git a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipeInfo.kt b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipeInfo.kt index 61dba90..4b7580a 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipeInfo.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipeInfo.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -31,7 +30,6 @@ import xyz.pixelatedw.recipe.data.RecipeWithTags import xyz.pixelatedw.recipe.data.RecipesView import xyz.pixelatedw.recipe.utils.parseMarkdown -@OptIn(ExperimentalMaterial3Api::class) @Composable fun RecipeInfo( ctx: MainActivity, @@ -124,7 +122,7 @@ fun RecipeInfo( when { openDeletionDialog.value -> { - RecipeDeletionDialog( + DeleteRecipeDialog( onAccept = { view.removeRecipe(active) ctx.db.recipeDao().delete(active.recipe) diff --git a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipePreview.kt b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipePreview.kt index 98ffa04..8000043 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipePreview.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipePreview.kt @@ -33,7 +33,7 @@ fun RecipePreview(entry: RecipeWithTags, previewUri: Bitmap?, onClick: () -> Uni bitmap = previewUri.asImageBitmap(), contentDescription = "Recipe image", contentScale = ContentScale.Crop, - modifier = Modifier.size(256.dp).padding(top = 16.dp, bottom = 16.dp) + modifier = Modifier.size(256.dp).padding(top = 16.dp, bottom = 16.dp), ) } } diff --git a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/TagFilterDialog.kt b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/TagFilterDialog.kt new file mode 100644 index 0000000..4f47848 --- /dev/null +++ b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/TagFilterDialog.kt @@ -0,0 +1,79 @@ +package xyz.pixelatedw.recipe.ui.components + +import androidx.collection.arraySetOf +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import xyz.pixelatedw.recipe.data.RecipesView +import xyz.pixelatedw.recipe.data.TagFilter + +@Composable +fun TagFilterDialog(onAccept: ( ) -> Unit, view: RecipesView) { + val filters = view.tagFilters.collectAsState() + + Dialog(onDismissRequest = onAccept) { + Card( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.9f) + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Text(text = "Filter by Tags", fontSize = 6.em, modifier = Modifier.padding(16.dp)) + LazyColumn( + modifier = Modifier + .fillMaxHeight(0.9f) + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .wrapContentSize(Alignment.TopStart)) { + items(filters.value) { tag -> + // TODO This doesn't really feel right lmao, but for now it works + val filterChecked = remember { mutableStateOf(tag.checked) } + + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = filterChecked.value, onCheckedChange = { + filterChecked.value = !tag.checked + view.setTagFilterState(tag.tag, !tag.checked) + }) + Text(tag.tag) + } + } + } + + Row(modifier = Modifier.fillMaxWidth().padding(end = 16.dp), horizontalArrangement = Arrangement.End) { + TextButton(onClick = onAccept) { + Text("Done") + } + } + } + } +}