Split the codebase more nicely

master
Wynd 2025-08-08 23:21:33 +03:00
parent 968691a8fe
commit 8854c39f13
10 changed files with 482 additions and 417 deletions

View File

@ -19,7 +19,6 @@
android:theme="@style/Theme.Recipe">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

View File

@ -1,86 +1,20 @@
package xyz.pixelatedw.recipe
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.util.Log
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.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
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.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import androidx.core.text.trimmedLength
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 org.commonmark.node.BulletList
import org.commonmark.node.Document
import org.commonmark.node.Emphasis
import org.commonmark.node.HardLineBreak
import org.commonmark.node.Heading
import org.commonmark.node.ListItem
import org.commonmark.node.Node
import org.commonmark.node.OrderedList
import org.commonmark.node.Paragraph
import org.commonmark.node.SoftLineBreak
import org.commonmark.node.StrongEmphasis
import org.commonmark.node.ThematicBreak
import org.commonmark.parser.Parser
import xyz.pixelatedw.recipe.data.RecipesView
import xyz.pixelatedw.recipe.ui.components.MainScreen
import xyz.pixelatedw.recipe.ui.theme.RecipeTheme
import java.io.BufferedReader
import java.io.InputStreamReader
import androidx.compose.material3.Typography
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.style.LineBreak
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import xyz.pixelatedw.recipe.utils.getRecipes
class MainActivity : ComponentActivity() {
private val recipeView: RecipesView by viewModels()
@ -90,365 +24,30 @@ class MainActivity : ComponentActivity() {
findSourceDir()
enableEdgeToEdge()
setContent {
enableEdgeToEdge()
setContent {
RecipeTheme {
Surface {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
RecipeList(innerPadding, recipeView)
MainScreen(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 intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
val getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri ->
getRecipes(uri)
val getContent =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri ->
getRecipes(this, recipeView, 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<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 text = reader.readText()
val lines = text.lines()
val sb = StringBuilder()
var hasToml = false
var frontMatterSize = 0
// TODO This could use some improvements as it always assumes frontmatter is the very first thing in the file
for (i in 0..lines.size) {
val line = lines[i]
frontMatterSize += line.trimmedLength() + 1
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 content = text.substring(frontMatterSize..<text.length)
val recipe = Recipe(
title = doc["title"]!!.asPrimitive().asString(),
tags = tags,
lastModified = lastModified,
content = content
)
return recipe
}
}
class RecipesView : ViewModel() {
private val _activeRecipe = MutableStateFlow<Recipe?>( null )
val activeRecipe = _activeRecipe.asStateFlow()
private val _recipes = MutableStateFlow<List<Recipe>>( arrayListOf() )
val recipes = _recipes.asStateFlow()
fun addRecipe(recipe: Recipe) {
_recipes.update {
it + recipe
}
}
fun setActive(recipe: Recipe) {
_activeRecipe.update { recipe }
}
}
data class Recipe(
val title: String,
val tags: List<String>,
val lastModified: Long,
val content: String
)
@Composable
fun RecipeList(padding: PaddingValues, view: RecipesView) {
val recipes = view.recipes.collectAsState()
val active = view.activeRecipe.collectAsState()
val navController = rememberNavController()
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")
})
}
}
}
composable("info") {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(padding)
) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = active.value!!.title,
style = MaterialTheme.typography.displayLarge,
textAlign = TextAlign.Center
)
}
Box(
modifier = Modifier.fillMaxWidth()
) {
val annotatedString =
parseMarkdown(active.value!!.content, MaterialTheme.typography)
Text(
text = annotatedString,
modifier = Modifier.padding(16.dp),
)
}
}
}
}
}
@Composable
fun RecipePreview(recipe: Recipe, 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()) {
Text(
text = recipe.title,
modifier = Modifier.fillMaxWidth(),
style = TextStyle(
textAlign = TextAlign.Center,
fontSize = 7.em,
)
)
}
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) {
for (tag in recipe.tags) {
Box(
modifier = Modifier
.padding(start = 8.dp)
.clip(RoundedCornerShape(percent = 50))
.background(androidx.compose.ui.graphics.Color(0, 153, 170))
) {
Text(
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
text = tag
)
}
}
}
}
}
//@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")), active)
// }
// }
//}
private fun parseMarkdown(
markdown: String,
typography: Typography,
): AnnotatedString {
val parser = Parser.builder().build()
val document = parser.parse(markdown)
val annotatedString = buildAnnotatedString {
visitMarkdownNode(document, typography)
}
return annotatedString.trim() as AnnotatedString
}
private fun AnnotatedString.Builder.visitMarkdownNode(
node: Node,
typography: Typography,
) {
val headingColor = androidx.compose.ui.graphics.Color.White
when (node) {
is Heading -> {
val style = when (node.level) {
in 1..3 -> typography.titleLarge
4 -> typography.titleMedium
5 -> typography.bodySmall
else -> typography.bodySmall
}
withStyle(style.toParagraphStyle().merge(ParagraphStyle(textAlign = TextAlign.Center))) {
withStyle(style.toSpanStyle().copy(color = headingColor)) {
visitChildren(node, typography)
appendLine()
}
}
}
is Paragraph -> {
if (node.parents.any { it is BulletList || it is OrderedList }) {
visitChildren(node, typography)
} else {
withStyle(typography.bodyLarge.toParagraphStyle()) {
visitChildren(node, typography)
appendLine()
}
}
}
is Emphasis -> {
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
visitChildren(node, typography)
}
}
is StrongEmphasis -> {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
visitChildren(node, typography)
}
}
is org.commonmark.node.Text -> {
append(node.literal)
visitChildren(node, typography)
}
is SoftLineBreak -> {
append(" ")
}
is HardLineBreak -> {
appendLine()
}
is ThematicBreak -> {
withStyle(ParagraphStyle(textAlign = TextAlign.Center)) {
withStyle(SpanStyle(letterSpacing = 0.sp)) {
appendLine("".repeat(10))
}
}
}
is OrderedList -> {
withStyle(ParagraphStyle(textIndent = TextIndent(firstLine = 10.sp, restLine = 20.sp))) {
visitChildren(node, typography)
}
}
is BulletList -> {
withStyle(ParagraphStyle(textIndent = TextIndent(firstLine = 10.sp, restLine = 20.sp))) {
visitChildren(node, typography)
}
}
is ListItem -> {
withStyle(ParagraphStyle(lineHeight = 18.sp)) {
if (node.parents.any { it is BulletList }) {
append("")
} else if (node.parents.any { it is OrderedList }) {
val startNumber = (node.parents.first { it is OrderedList } as OrderedList).markerStartNumber
val index = startNumber + node.previousSiblings.filterIsInstance<ListItem>().size
append("$index. ")
}
visitChildren(node, typography)
appendLine()
}
}
is Document -> {
visitChildren(node, typography)
}
else -> {
Log.e("MarkdownText", "Traversing unhandled node: $node")
visitChildren(node, typography)
}
}
}
private fun AnnotatedString.Builder.visitChildren(
node: Node,
typography: Typography,
) {
var child = node.firstChild
while (child != null) {
visitMarkdownNode(child, typography)
child = child.next
}
}
private val Node.parents: List<Node> get() {
val list = mutableListOf<Node>()
var current = this
while (true) {
current = current.parent ?: return list
list += current
}
}
private val Node.previousSiblings: List<Node> get() {
val list = mutableListOf<Node>()
var current = this
while (true) {
current = current.previous ?: return list
list += current
}
}

View File

@ -0,0 +1,8 @@
package xyz.pixelatedw.recipe.data
data class Recipe(
val title: String,
val tags: List<String>,
val lastModified: Long,
val content: String
)

View File

@ -0,0 +1,24 @@
package xyz.pixelatedw.recipe.data
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class RecipesView : ViewModel() {
private val _activeRecipe = MutableStateFlow<Recipe?>( null )
val activeRecipe = _activeRecipe.asStateFlow()
private val _recipes = MutableStateFlow<List<Recipe>>( arrayListOf() )
val recipes = _recipes.asStateFlow()
fun addRecipe(recipe: Recipe) {
_recipes.update {
it + recipe
}
}
fun setActive(recipe: Recipe) {
_activeRecipe.update { recipe }
}
}

View File

@ -0,0 +1,40 @@
package xyz.pixelatedw.recipe.ui.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import xyz.pixelatedw.recipe.data.RecipesView
@Composable
fun MainScreen(padding: PaddingValues, view: RecipesView) {
val recipes = view.recipes.collectAsState()
val active = view.activeRecipe.collectAsState()
val navController = rememberNavController()
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")
})
}
}
}
composable("info") {
RecipeInfo(padding, active.value!!)
}
}
}

View File

@ -0,0 +1,50 @@
package xyz.pixelatedw.recipe.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import xyz.pixelatedw.recipe.data.Recipe
import xyz.pixelatedw.recipe.utils.parseMarkdown
@Composable
fun RecipeInfo(padding: PaddingValues, active: Recipe) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(padding)
) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = active.title,
style = MaterialTheme.typography.displayLarge,
textAlign = TextAlign.Center
)
}
Box(
modifier = Modifier.fillMaxWidth()
) {
val annotatedString =
parseMarkdown(active.content, MaterialTheme.typography)
Text(
text = annotatedString,
modifier = Modifier.padding(16.dp),
)
}
}
}

View File

@ -0,0 +1,50 @@
package xyz.pixelatedw.recipe.ui.components
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.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.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) {
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()) {
Text(
text = recipe.title,
modifier = Modifier.fillMaxWidth(),
style = TextStyle(
textAlign = TextAlign.Center,
fontSize = 7.em,
)
)
}
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) {
for (tag in recipe.tags) {
Tag(tag)
}
}
}
}

View File

@ -0,0 +1,27 @@
package xyz.pixelatedw.recipe.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun Tag(tag: String) {
Box(
modifier = Modifier
.padding(start = 8.dp)
.clip(RoundedCornerShape(percent = 50))
.background(Color(0, 153, 170))
) {
Text(
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
text = tag
)
}
}

View File

@ -0,0 +1,191 @@
package xyz.pixelatedw.recipe.utils
import android.util.Log
import androidx.compose.material3.Typography
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.sp
import org.commonmark.node.BulletList
import org.commonmark.node.Document
import org.commonmark.node.Emphasis
import org.commonmark.node.HardLineBreak
import org.commonmark.node.Heading
import org.commonmark.node.ListItem
import org.commonmark.node.Node
import org.commonmark.node.OrderedList
import org.commonmark.node.Paragraph
import org.commonmark.node.SoftLineBreak
import org.commonmark.node.StrongEmphasis
import org.commonmark.node.Text
import org.commonmark.node.ThematicBreak
import org.commonmark.parser.Parser
fun parseMarkdown(
markdown: String,
typography: Typography,
): AnnotatedString {
val parser = Parser.builder().build()
val document = parser.parse(markdown)
val annotatedString = buildAnnotatedString {
visitMarkdownNode(document, typography)
}
return annotatedString.trim() as AnnotatedString
}
private fun AnnotatedString.Builder.visitMarkdownNode(
node: Node,
typography: Typography,
) {
val headingColor = androidx.compose.ui.graphics.Color.White
when (node) {
is Heading -> {
val style = when (node.level) {
in 1..3 -> typography.titleLarge
4 -> typography.titleMedium
5 -> typography.bodySmall
else -> typography.bodySmall
}
withStyle(
style.toParagraphStyle().merge(ParagraphStyle(textAlign = TextAlign.Center))
) {
withStyle(style.toSpanStyle().copy(color = headingColor)) {
visitChildren(node, typography)
appendLine()
}
}
}
is Paragraph -> {
if (node.parents.any { it is BulletList || it is OrderedList }) {
visitChildren(node, typography)
} else {
withStyle(typography.bodyLarge.toParagraphStyle()) {
visitChildren(node, typography)
appendLine()
}
}
}
is Emphasis -> {
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
visitChildren(node, typography)
}
}
is StrongEmphasis -> {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
visitChildren(node, typography)
}
}
is Text -> {
append(node.literal)
visitChildren(node, typography)
}
is SoftLineBreak -> {
append(" ")
}
is HardLineBreak -> {
appendLine()
}
is ThematicBreak -> {
withStyle(ParagraphStyle(textAlign = TextAlign.Center)) {
withStyle(SpanStyle(letterSpacing = 0.sp)) {
appendLine("".repeat(10))
}
}
}
is OrderedList -> {
withStyle(
ParagraphStyle(
textIndent = TextIndent(
firstLine = 10.sp,
restLine = 20.sp
)
)
) {
visitChildren(node, typography)
}
}
is BulletList -> {
withStyle(
ParagraphStyle(
textIndent = TextIndent(
firstLine = 10.sp,
restLine = 20.sp
)
)
) {
visitChildren(node, typography)
}
}
is ListItem -> {
withStyle(ParagraphStyle(lineHeight = 18.sp)) {
if (node.parents.any { it is BulletList }) {
append("")
} else if (node.parents.any { it is OrderedList }) {
val startNumber =
(node.parents.first { it is OrderedList } as OrderedList).markerStartNumber
val index =
startNumber + node.previousSiblings.filterIsInstance<ListItem>().size
append("$index. ")
}
visitChildren(node, typography)
appendLine()
}
}
is Document -> {
visitChildren(node, typography)
}
else -> {
Log.e("MarkdownText", "Traversing unhandled node: $node")
visitChildren(node, typography)
}
}
}
private fun AnnotatedString.Builder.visitChildren(
node: Node,
typography: Typography,
) {
var child = node.firstChild
while (child != null) {
visitMarkdownNode(child, typography)
child = child.next
}
}
private val Node.parents: List<Node>
get() {
val list = mutableListOf<Node>()
var current = this
while (true) {
current = current.parent ?: return list
list += current
}
}
private val Node.previousSiblings: List<Node>
get() {
val list = mutableListOf<Node>()
var current = this
while (true) {
current = current.previous ?: return list
list += current
}
}

View File

@ -0,0 +1,77 @@
package xyz.pixelatedw.recipe.utils
import android.content.Context
import android.net.Uri
import androidx.core.text.trimmedLength
import androidx.documentfile.provider.DocumentFile
import io.github.wasabithumb.jtoml.JToml
import xyz.pixelatedw.recipe.data.Recipe
import xyz.pixelatedw.recipe.data.RecipesView
import java.io.BufferedReader
import java.io.InputStreamReader
fun getRecipes(ctx: Context, view: RecipesView, uri: Uri?) {
if (uri == null) {
return
}
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)
}
}
}
}
}
private fun parseRecipe(ctx: Context, file: DocumentFile): Recipe? {
val lastModified = file.lastModified()
val inputStream = ctx.contentResolver.openInputStream(file.uri)
val reader = BufferedReader(InputStreamReader(inputStream))
val text = reader.readText()
val lines = text.lines()
val sb = StringBuilder()
var hasToml = false
var frontMatterSize = 0
// TODO This could use some improvements as it always assumes frontmatter is the very first thing in the file
for (i in 0..lines.size) {
val line = lines[i]
frontMatterSize += line.trimmedLength() + 1
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 content = text.substring(frontMatterSize..<text.length)
val recipe = Recipe(
title = doc["title"]!!.asPrimitive().asString(),
tags = tags,
lastModified = lastModified,
content = content
)
return recipe
}