Added searching and image support for previews

master
Wynd 2025-08-09 13:21:06 +03:00
parent 8854c39f13
commit 0af796082c
7 changed files with 118 additions and 23 deletions

View File

@ -23,5 +23,4 @@
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -25,6 +25,7 @@ class MainActivity : ComponentActivity() {
findSourceDir()
enableEdgeToEdge()
setContent {
RecipeTheme {
Surface {

View File

@ -1,8 +1,15 @@
package xyz.pixelatedw.recipe.data
import android.graphics.Bitmap
data class Recipe(
val title: String,
val tags: List<String>,
val previews: List<Bitmap>,
val lastModified: Long,
val content: String
)
) {
fun mainImage(): Bitmap? {
return this.previews.getOrNull(0)
}
}

View File

@ -12,6 +12,13 @@ class RecipesView : ViewModel() {
private val _recipes = MutableStateFlow<List<Recipe>>( arrayListOf() )
val recipes = _recipes.asStateFlow()
private val _search = MutableStateFlow<String?>(null)
val search = _search.asStateFlow()
fun setSearch(search: String) {
_search.update { search }
}
fun addRecipe(recipe: Recipe) {
_recipes.update {
it + recipe

View File

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

View File

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

View File

@ -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<String, Bitmap>()
private val recipeFiles = mutableListOf<DocumentFile>()
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<DocumentFile> = 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<DocumentFile> = 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<String>()
for (tomlElem in doc["pics"]!!.asArray()) {
pics.add(tomlElem!!.asPrimitive().asString())
}
val recipePreviews = mutableListOf<Bitmap>()
for (pic in pics) {
val bitmap = previews[pic]
if (bitmap != null) {
recipePreviews.add(bitmap)
}
}
val content = text.substring(frontMatterSize..<text.length)
val recipe = Recipe(
title = doc["title"]!!.asPrimitive().asString(),
tags = tags,
previews = recipePreviews,
lastModified = lastModified,
content = content
)
reader.close()
return recipe
}