package xyz.pixelatedw.recipe import android.app.Activity import android.content.Intent import android.content.res.Configuration import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModel import io.github.wasabithumb.jtoml.JToml import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import xyz.pixelatedw.recipe.ui.theme.RecipeTheme import java.io.BufferedReader import java.io.InputStreamReader class MainActivity : ComponentActivity() { private val recipeView: RecipesView by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) findSourceDir() enableEdgeToEdge() setContent { RecipeTheme { Surface { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> RecipeList(innerPadding, recipeView) } } } } } private fun findSourceDir() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addCategory(Intent.CATEGORY_DEFAULT) } val getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> getRecipes(uri) } } } getContent.launch(intent) } private fun getRecipes(uri: Uri?) { if (uri == null) { return } val dir = DocumentFile.fromTreeUri(this, uri) if (dir != null) { val fileList: Array = dir.listFiles() for (file in fileList) { if (file.isFile && file.name?.endsWith(".md") == true) { val recipe = parseRecipe(file) if (recipe != null) { this.recipeView.addRecipe(recipe) } } } } } private fun parseRecipe(file: DocumentFile): Recipe? { val lastModified = file.lastModified() val inputStream = this.contentResolver.openInputStream(file.uri) val reader = BufferedReader(InputStreamReader(inputStream)) val lines = reader.readLines() val sb = StringBuilder() var hasToml = false for (i in 0..lines.size) { val line = lines[i] if (line == "+++") { if (hasToml) { break } hasToml = true continue } sb.appendLine(line) } val toml = JToml.jToml() val doc = toml.readFromString(sb.toString()) val tags = arrayListOf() for (tomlElem in doc["tags"]!!.asArray()) { tags.add(tomlElem!!.asPrimitive().asString()) } val recipe = Recipe( title = doc["title"]!!.asPrimitive().asString(), tags = tags ) return recipe } } class RecipesView : ViewModel() { private val _recipes = MutableStateFlow>( arrayListOf() ) val recipes = _recipes.asStateFlow() public fun addRecipe(recipe: Recipe) { _recipes.update { it + recipe } } } data class Recipe(val title: String, val tags: List) @Composable fun RecipeList(padding: PaddingValues, view: RecipesView) { val recipes = view.recipes.collectAsState() LazyColumn(modifier = Modifier.padding(padding)) { items(recipes.value) { recipe -> RecipePreview(recipe) } } } //@Preview //@Composable //fun RecipeListPreview() { // val recipes = listOf( // Recipe(title = "Test", tags = listOf("tag1", "tag2")), // Recipe(title = "Actual Recipe", tags = listOf("test")) // ) //// RecipeTheme { //// RecipeList(PaddingValues(), recipes) //// } //} @Composable fun RecipePreview(recipe: Recipe) { Column(modifier = Modifier.padding(8.dp)) { Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { Image( painter = painterResource(R.drawable.ic_launcher_background), contentDescription = "Recipe image", modifier = Modifier.size(256.dp) ) } Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { Text( text = recipe.title, modifier = Modifier.fillMaxWidth(), style = TextStyle( textAlign = TextAlign.Center, fontSize = 7.em, ) ) } Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) { for (tag in recipe.tags) { Text( text = tag, modifier = Modifier.padding(start = 8.dp) ) } } } } @Preview( showBackground = true, name = "Light Mode" ) @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun RecipePreviewPreview() { RecipeTheme { Surface { RecipePreview(Recipe("Test", listOf("test", "test2", "test3"))) } } }