Finally some state exists and can load recipes from the file system

master
Wynd 2025-08-07 16:47:41 +03:00
parent 65d46a63ee
commit 8229d7a6f0
3 changed files with 116 additions and 37 deletions

View File

@ -40,7 +40,6 @@ android {
} }
dependencies { dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
@ -49,6 +48,7 @@ dependencies {
implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.documentfile)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
@ -56,4 +56,6 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.jtoml)
} }

View File

@ -9,99 +9,172 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em 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 xyz.pixelatedw.recipe.ui.theme.RecipeTheme
import androidx.compose.foundation.lazy.items import java.io.BufferedReader
import androidx.compose.ui.platform.LocalContext import java.io.InputStreamReader
import androidx.core.net.toUri
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val recipeView: RecipesView by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val recipes = listOf( findSourceDir()
Recipe(title = "Test", tags = listOf("tag1", "tag2")),
Recipe(title = "Actual Recipe", tags = listOf("test"))
)
requestDirLauncher()
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
RecipeTheme { RecipeTheme {
Surface { Surface {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
RecipeList(innerPadding, recipes) RecipeList(innerPadding, recipeView)
} }
} }
} }
} }
} }
private val requestDirLauncher = { private fun findSourceDir() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) 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 -> val getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri -> result.data?.data?.let { uri ->
println(uri) getRecipes(uri)
} }
} }
} }
getContent.launch(intent) getContent.launch(intent)
} }
private fun getRecipes(uri: Uri?) {
if (uri == null) {
return
}
val dir = DocumentFile.fromTreeUri(this, 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(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<String>()
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<List<Recipe>>( arrayListOf() )
val recipes = _recipes.asStateFlow()
public fun addRecipe(recipe: Recipe) {
_recipes.update {
it + recipe
}
}
} }
data class Recipe(val title: String, val tags: List<String>) data class Recipe(val title: String, val tags: List<String>)
@Composable @Composable
fun RecipeList(padding: PaddingValues, recipes: List<Recipe>) { fun RecipeList(padding: PaddingValues, view: RecipesView) {
val recipes = view.recipes.collectAsState()
LazyColumn(modifier = Modifier.padding(padding)) { LazyColumn(modifier = Modifier.padding(padding)) {
items(recipes) { recipe -> items(recipes.value) { recipe ->
RecipePreview(recipe) RecipePreview(recipe)
} }
} }
} }
@Preview //@Preview
@Composable //@Composable
fun RecipeListPreview() { //fun RecipeListPreview() {
val recipes = listOf( // val recipes = listOf(
Recipe(title = "Test", tags = listOf("tag1", "tag2")), // Recipe(title = "Test", tags = listOf("tag1", "tag2")),
Recipe(title = "Actual Recipe", tags = listOf("test")) // Recipe(title = "Actual Recipe", tags = listOf("test"))
) // )
RecipeTheme { //// RecipeTheme {
RecipeList(PaddingValues(), recipes) //// RecipeList(PaddingValues(), recipes)
} //// }
} //}
@Composable @Composable
fun RecipePreview(recipe: Recipe) { fun RecipePreview(recipe: Recipe) {

View File

@ -1,5 +1,6 @@
[versions] [versions]
agp = "8.7.3" agp = "8.7.3"
jtoml = "1.1.0"
kotlin = "2.0.0" kotlin = "2.0.0"
coreKtx = "1.16.0" coreKtx = "1.16.0"
junit = "4.13.2" junit = "4.13.2"
@ -8,9 +9,11 @@ espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.2" lifecycleRuntimeKtx = "2.9.2"
activityCompose = "1.10.1" activityCompose = "1.10.1"
composeBom = "2024.04.01" composeBom = "2024.04.01"
documentfile = "1.1.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
jtoml = { group = "io.github.wasabithumb", name = "jtoml", version.ref = "jtoml" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@ -24,6 +27,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }