From 0af796082c3f22d1e374efa00f57719089d8345a Mon Sep 17 00:00:00 2001 From: Wynd Date: Sat, 9 Aug 2025 13:21:06 +0300 Subject: [PATCH] Added searching and image support for previews --- app/src/main/AndroidManifest.xml | 1 - .../xyz/pixelatedw/recipe/MainActivity.kt | 1 + .../java/xyz/pixelatedw/recipe/data/Recipe.kt | 9 ++- .../xyz/pixelatedw/recipe/data/RecipesView.kt | 7 +++ .../recipe/ui/components/MainScreen.kt | 37 +++++++++-- .../recipe/ui/components/RecipePreview.kt | 23 ++++--- .../pixelatedw/recipe/utils/RecipeParser.kt | 63 +++++++++++++++++-- 7 files changed, 118 insertions(+), 23 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0a05bbb..069ae9a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,5 +23,4 @@ - diff --git a/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt b/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt index afc8483..81476f7 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt @@ -25,6 +25,7 @@ class MainActivity : ComponentActivity() { findSourceDir() enableEdgeToEdge() + setContent { RecipeTheme { Surface { diff --git a/app/src/main/java/xyz/pixelatedw/recipe/data/Recipe.kt b/app/src/main/java/xyz/pixelatedw/recipe/data/Recipe.kt index 609464b..50cbdf1 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/data/Recipe.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/data/Recipe.kt @@ -1,8 +1,15 @@ package xyz.pixelatedw.recipe.data +import android.graphics.Bitmap + data class Recipe( val title: String, val tags: List, + val previews: List, val lastModified: Long, val content: String -) +) { + fun mainImage(): Bitmap? { + return this.previews.getOrNull(0) + } +} 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 eb3dd57..da2aa3e 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/data/RecipesView.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/data/RecipesView.kt @@ -12,6 +12,13 @@ class RecipesView : ViewModel() { private val _recipes = MutableStateFlow>( arrayListOf() ) val recipes = _recipes.asStateFlow() + private val _search = MutableStateFlow(null) + val search = _search.asStateFlow() + + fun setSearch(search: String) { + _search.update { search } + } + fun addRecipe(recipe: Recipe) { _recipes.update { it + recipe 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 62611e3..1060629 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 @@ -1,35 +1,60 @@ package xyz.pixelatedw.recipe.ui.components +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +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.OutlinedTextField import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import xyz.pixelatedw.recipe.data.Recipe import xyz.pixelatedw.recipe.data.RecipesView @Composable fun MainScreen(padding: PaddingValues, view: RecipesView) { val recipes = view.recipes.collectAsState() val active = view.activeRecipe.collectAsState() + val search = view.search.collectAsState() val navController = rememberNavController() + val isInSearch = isInSearch@{ recipe: Recipe -> + val hasTitle = recipe.title.contains(search.value.orEmpty(), ignoreCase = true) + val hasTags = recipe.tags.isNotEmpty() && recipe.tags.stream() + .filter { tag -> tag.contains(search.value.orEmpty(), ignoreCase = true) }.count() > 0 + + hasTitle || hasTags + } + NavHost( navController = navController, startDestination = "list" ) { composable("list") { - LazyColumn(modifier = Modifier.padding(padding)) { - items(recipes.value) { recipe -> - RecipePreview(recipe, onClick = { - view.setActive(recipe) - navController.navigate("info") - }) + Column(modifier = Modifier.padding(padding)) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + value = search.value.orEmpty(), + onValueChange = { search -> view.setSearch(search) }) + LazyColumn { + items(recipes.value) { recipe -> + if (isInSearch(recipe)) { + val previewUri = recipe.mainImage() + RecipePreview(recipe, previewUri, onClick = { + view.setActive(recipe) + navController.navigate("info") + }) + } + } } } } 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 77ae76b..8db9312 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 @@ -1,35 +1,40 @@ package xyz.pixelatedw.recipe.ui.components +import android.graphics.Bitmap import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em -import xyz.pixelatedw.recipe.R import xyz.pixelatedw.recipe.data.Recipe @Composable -fun RecipePreview(recipe: Recipe, onClick: () -> Unit) { +fun RecipePreview(recipe: Recipe, previewUri: Bitmap?, onClick: () -> Unit) { Column(modifier = Modifier .padding(8.dp) .clickable(onClick = onClick)) { - Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { - Image( - painter = painterResource(R.drawable.ic_launcher_background), - contentDescription = "Recipe image", - modifier = Modifier.size(256.dp).padding(top = 16.dp, bottom = 16.dp) - ) + Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth().height(256.dp)) { + if (previewUri != null) { + Image( + bitmap = previewUri.asImageBitmap(), + contentDescription = "Recipe image", + contentScale = ContentScale.Crop, + modifier = Modifier.size(256.dp).padding(top = 16.dp, bottom = 16.dp) + ) + } } Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { Text( diff --git a/app/src/main/java/xyz/pixelatedw/recipe/utils/RecipeParser.kt b/app/src/main/java/xyz/pixelatedw/recipe/utils/RecipeParser.kt index 8cc1dbe..b1df2ab 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/utils/RecipeParser.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/utils/RecipeParser.kt @@ -1,6 +1,8 @@ package xyz.pixelatedw.recipe.utils import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.net.Uri import androidx.core.text.trimmedLength import androidx.documentfile.provider.DocumentFile @@ -10,6 +12,9 @@ import xyz.pixelatedw.recipe.data.RecipesView import java.io.BufferedReader import java.io.InputStreamReader +private val previews = HashMap() +private val recipeFiles = mutableListOf() + fun getRecipes(ctx: Context, view: RecipesView, uri: Uri?) { if (uri == null) { return @@ -17,14 +22,44 @@ fun getRecipes(ctx: Context, view: RecipesView, uri: Uri?) { val dir = DocumentFile.fromTreeUri(ctx, uri) if (dir != null) { - val fileList: Array = dir.listFiles() - for (file in fileList) { - if (file.isFile && file.name?.endsWith(".md") == true) { - val recipe = parseRecipe(ctx, file) - if (recipe != null) { - view.addRecipe(recipe) + parseDir(ctx, dir) + + for (file in recipeFiles) { + val recipe = parseRecipe(ctx, file) + if (recipe != null) { + view.addRecipe(recipe) + } + } + } +} + +fun parseDir(ctx: Context, dir: DocumentFile) { + val fileList: Array = dir.listFiles() + for (file in fileList) { + if(file.isDirectory) { + parseDir(ctx, file) + continue; + } + + if (file.isFile && file.name?.endsWith(".jpg") == true) { + val inStream = ctx.contentResolver.openInputStream(file.uri) + + var bitmap: Bitmap? = null + if (file.exists()) { + try { + bitmap = BitmapFactory.decodeStream(inStream) + } + finally { + inStream?.close() } } + + if (bitmap != null) { + previews[file.name!!] = bitmap + } + } + if (file.isFile && file.name?.endsWith(".md") == true) { + recipeFiles.add(file) } } } @@ -64,14 +99,30 @@ private fun parseRecipe(ctx: Context, file: DocumentFile): Recipe? { tags.add(tomlElem!!.asPrimitive().asString()) } + val pics = arrayListOf() + for (tomlElem in doc["pics"]!!.asArray()) { + pics.add(tomlElem!!.asPrimitive().asString()) + } + + val recipePreviews = mutableListOf() + for (pic in pics) { + val bitmap = previews[pic] + if (bitmap != null) { + recipePreviews.add(bitmap) + } + } + val content = text.substring(frontMatterSize..