Add a demo for tag filtering, still heavily WIP

master
Wynd 2025-09-08 01:48:54 +03:00
parent e996254451
commit 67659a0af6
7 changed files with 166 additions and 17 deletions

View File

@ -12,18 +12,35 @@ class RecipesView : ViewModel() {
private val _recipes = MutableStateFlow<List<RecipeWithTags>>( arrayListOf() )
val recipes = _recipes.asStateFlow()
private val _tagFilters = MutableStateFlow<List<TagFilter>>( arrayListOf() )
val tagFilters = _tagFilters.asStateFlow()
private val _search = MutableStateFlow<String?>(null)
val search = _search.asStateFlow()
private val _keepScreenOn = MutableStateFlow<Boolean>(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<RecipeWithTags>) {
_recipes.update { recipes }
val filters = arrayListOf<TagFilter>()
_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) {

View File

@ -0,0 +1,4 @@
package xyz.pixelatedw.recipe.data
data class TagFilter(val tag: String, var checked: Boolean = false) {
}

View File

@ -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

View File

@ -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!!)

View File

@ -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)

View File

@ -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),
)
}
}

View File

@ -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")
}
}
}
}
}