From 0b2e301f8a866741859a369f0db6056c2ab6980b Mon Sep 17 00:00:00 2001 From: Wynd Date: Sat, 9 Aug 2025 18:09:09 +0300 Subject: [PATCH] Caching loaded recipes in database, copying files to app storage for easier handling and some fixes --- app/build.gradle.kts | 19 ++-- .../xyz/pixelatedw/recipe/MainActivity.kt | 33 +++--- .../xyz/pixelatedw/recipe/data/AppDatabase.kt | 16 +++ .../java/xyz/pixelatedw/recipe/data/Recipe.kt | 34 +++++- .../pixelatedw/recipe/data/RecipeWithTags.kt | 44 ++++++++ .../xyz/pixelatedw/recipe/data/RecipesView.kt | 16 ++- .../java/xyz/pixelatedw/recipe/data/Tag.kt | 18 ++++ .../recipe/ui/components/MainScreen.kt | 57 +++++++--- .../recipe/ui/components/RecipeInfo.kt | 7 +- .../recipe/ui/components/RecipePreview.kt | 7 +- .../pixelatedw/recipe/ui/components/Tag.kt | 5 +- .../pixelatedw/recipe/utils/RecipeParser.kt | 101 ++++++++++-------- build.gradle.kts | 1 + gradle/libs.versions.toml | 7 +- 14 files changed, 262 insertions(+), 103 deletions(-) create mode 100644 app/src/main/java/xyz/pixelatedw/recipe/data/AppDatabase.kt create mode 100644 app/src/main/java/xyz/pixelatedw/recipe/data/RecipeWithTags.kt create mode 100644 app/src/main/java/xyz/pixelatedw/recipe/data/Tag.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c3f9a28..3ffb2b0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.devtools.ksp) } android { @@ -49,18 +50,22 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.documentfile) + implementation(libs.androidx.navigation.fragment) + implementation(libs.androidx.navigation.navigation.ui) + implementation(libs.androidx.navigation.navigation.compose) + implementation(libs.androidx.room) + implementation(libs.jtoml) + implementation(libs.commonmark) + + ksp(libs.androidx.room.compiler) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) - - implementation(libs.androidx.navigation.fragment) - implementation(libs.androidx.navigation.navigation.ui) - implementation(libs.androidx.navigation.navigation.compose) - - implementation(libs.jtoml) - implementation(libs.commonmark) } diff --git a/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt b/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt index 81476f7..f1e3ff2 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/MainActivity.kt @@ -1,6 +1,5 @@ package xyz.pixelatedw.recipe -import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -11,6 +10,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.ui.Modifier +import androidx.room.Room +import xyz.pixelatedw.recipe.data.AppDatabase import xyz.pixelatedw.recipe.data.RecipesView import xyz.pixelatedw.recipe.ui.components.MainScreen import xyz.pixelatedw.recipe.ui.theme.RecipeTheme @@ -18,11 +19,18 @@ import xyz.pixelatedw.recipe.utils.getRecipes class MainActivity : ComponentActivity() { private val recipeView: RecipesView by viewModels() + private lateinit var db: AppDatabase override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - findSourceDir() + db = Room.databaseBuilder( + this, + AppDatabase::class.java, "recipes" + ).allowMainThreadQueries().build() + + val recipes = db.recipeWithTagsDao().getAll() + recipeView.setRecipes(recipes) enableEdgeToEdge() @@ -30,25 +38,22 @@ class MainActivity : ComponentActivity() { RecipeTheme { Surface { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - MainScreen(innerPadding, recipeView) + MainScreen(this, innerPadding, recipeView) } } } } } - private fun findSourceDir() { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + val sourceChooser = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + result.data?.data?.let { uri -> + getRecipes(this, db, uri) - val getContent = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - result.data?.data?.let { uri -> - getRecipes(this, recipeView, uri) - } + val recipes = db.recipeWithTagsDao().getAll() + recipeView.setRecipes(recipes) } } - - getContent.launch(intent) - } + } } diff --git a/app/src/main/java/xyz/pixelatedw/recipe/data/AppDatabase.kt b/app/src/main/java/xyz/pixelatedw/recipe/data/AppDatabase.kt new file mode 100644 index 0000000..48c85cd --- /dev/null +++ b/app/src/main/java/xyz/pixelatedw/recipe/data/AppDatabase.kt @@ -0,0 +1,16 @@ +package xyz.pixelatedw.recipe.data + +import androidx.room.Database +import androidx.room.RoomDatabase + +@Database( + version = 1, + entities = [Recipe::class, Tag::class, RecipeTag::class], +) +abstract class AppDatabase : RoomDatabase() { + abstract fun recipeWithTagsDao(): RecipeWithTagsDao + + abstract fun recipeDao(): RecipeDao + + abstract fun tagDao(): TagDao +} 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 50cbdf1..c7b3775 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/data/Recipe.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/data/Recipe.kt @@ -1,15 +1,41 @@ package xyz.pixelatedw.recipe.data +import android.content.Context import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.core.net.toUri +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import java.io.File +@Entity data class Recipe( + @PrimaryKey val title: String, - val tags: List, - val previews: List, + val preview: String?, val lastModified: Long, val content: String ) { - fun mainImage(): Bitmap? { - return this.previews.getOrNull(0) + fun mainImage(ctx: Context): Bitmap? { + if (this.preview != null) { + val file = File(ctx.filesDir, this.preview) + if (file.exists()) { + ctx.contentResolver.openInputStream(file.toUri()).use { + val bitmap: Bitmap? = BitmapFactory.decodeStream(it) + return bitmap + } + } + } + + return null } } + +@Dao +interface RecipeDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(recipe: Recipe) +} diff --git a/app/src/main/java/xyz/pixelatedw/recipe/data/RecipeWithTags.kt b/app/src/main/java/xyz/pixelatedw/recipe/data/RecipeWithTags.kt new file mode 100644 index 0000000..07a8afc --- /dev/null +++ b/app/src/main/java/xyz/pixelatedw/recipe/data/RecipeWithTags.kt @@ -0,0 +1,44 @@ +package xyz.pixelatedw.recipe.data + +import androidx.room.Dao +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.Junction +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Relation +import androidx.room.Transaction + +@Entity(primaryKeys = ["title", "name"]) +data class RecipeTag( + val title: String, + val name: String +) + +data class RecipeWithTags( + @Embedded + val recipe: Recipe, + + @Relation( + parentColumn = "title", + entity = Tag::class, + entityColumn = "name", + associateBy = Junction( + value = RecipeTag::class, + parentColumn = "title", + entityColumn = "name" + ) + ) + val tags: List +) + +@Dao +interface RecipeWithTagsDao { + @Transaction + @Query("SELECT * FROM recipe") + fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(recipe: RecipeTag) +} 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 da2aa3e..b7b4072 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/data/RecipesView.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/data/RecipesView.kt @@ -6,26 +6,24 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update class RecipesView : ViewModel() { - private val _activeRecipe = MutableStateFlow( null ) + private val _activeRecipe = MutableStateFlow( null ) val activeRecipe = _activeRecipe.asStateFlow() - private val _recipes = MutableStateFlow>( arrayListOf() ) + private val _recipes = MutableStateFlow>( arrayListOf() ) val recipes = _recipes.asStateFlow() private val _search = MutableStateFlow(null) val search = _search.asStateFlow() + fun setRecipes(recipes: List) { + _recipes.update { recipes } + } + fun setSearch(search: String) { _search.update { search } } - fun addRecipe(recipe: Recipe) { - _recipes.update { - it + recipe - } - } - - fun setActive(recipe: Recipe) { + fun setActive(recipe: RecipeWithTags) { _activeRecipe.update { recipe } } } diff --git a/app/src/main/java/xyz/pixelatedw/recipe/data/Tag.kt b/app/src/main/java/xyz/pixelatedw/recipe/data/Tag.kt new file mode 100644 index 0000000..7ea9d62 --- /dev/null +++ b/app/src/main/java/xyz/pixelatedw/recipe/data/Tag.kt @@ -0,0 +1,18 @@ +package xyz.pixelatedw.recipe.data + +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey + +@Entity +data class Tag( + @PrimaryKey val name: String +) + +@Dao +interface TagDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(tag: Tag) +} 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 1060629..348ff8a 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,34 +1,44 @@ package xyz.pixelatedw.recipe.ui.components +import android.content.Intent +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.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign 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.MainActivity +import xyz.pixelatedw.recipe.data.RecipeWithTags import xyz.pixelatedw.recipe.data.RecipesView @Composable -fun MainScreen(padding: PaddingValues, view: RecipesView) { +fun MainScreen(ctx: MainActivity, 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 + val isInSearch = isInSearch@{ entry: RecipeWithTags -> + val hasTitle = entry.recipe.title.contains(search.value.orEmpty(), ignoreCase = true) + val hasTags = entry.tags.isNotEmpty() && entry.tags.stream() + .filter { tag -> tag.name.contains(search.value.orEmpty(), ignoreCase = true) } + .count() > 0 hasTitle || hasTags } @@ -39,18 +49,33 @@ fun MainScreen(padding: PaddingValues, view: RecipesView) { ) { composable("list") { Column(modifier = Modifier.padding(padding)) { - OutlinedTextField( + Row( modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - value = search.value.orEmpty(), - onValueChange = { search -> view.setSearch(search) }) + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + OutlinedTextField( + value = search.value.orEmpty(), + onValueChange = { search -> view.setSearch(search) }, + ) + Button( + onClick = { ctx.sourceChooser.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)) }, + ) { + Text( + text = "Load", + maxLines = 1, + textAlign = TextAlign.Center, + ) + } + } LazyColumn { - items(recipes.value) { recipe -> - if (isInSearch(recipe)) { - val previewUri = recipe.mainImage() - RecipePreview(recipe, previewUri, onClick = { - view.setActive(recipe) + items(recipes.value) { entry -> + if (isInSearch(entry)) { + val previewUri = entry.recipe.mainImage(LocalContext.current) + RecipePreview(entry, previewUri, onClick = { + view.setActive(entry) navController.navigate("info") }) } diff --git a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipeInfo.kt b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipeInfo.kt index 1d2373b..550cbe3 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipeInfo.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/RecipeInfo.kt @@ -16,10 +16,11 @@ 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.data.RecipeWithTags import xyz.pixelatedw.recipe.utils.parseMarkdown @Composable -fun RecipeInfo(padding: PaddingValues, active: Recipe) { +fun RecipeInfo(padding: PaddingValues, active: RecipeWithTags) { Column( modifier = Modifier .verticalScroll(rememberScrollState()) @@ -30,7 +31,7 @@ fun RecipeInfo(padding: PaddingValues, active: Recipe) { modifier = Modifier.fillMaxWidth() ) { Text( - text = active.title, + text = active.recipe.title, style = MaterialTheme.typography.displayLarge, textAlign = TextAlign.Center ) @@ -40,7 +41,7 @@ fun RecipeInfo(padding: PaddingValues, active: Recipe) { modifier = Modifier.fillMaxWidth() ) { val annotatedString = - parseMarkdown(active.content, MaterialTheme.typography) + parseMarkdown(active.recipe.content, MaterialTheme.typography) Text( text = annotatedString, modifier = Modifier.padding(16.dp), 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 8db9312..98ffa04 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 @@ -20,9 +20,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import xyz.pixelatedw.recipe.data.Recipe +import xyz.pixelatedw.recipe.data.RecipeWithTags @Composable -fun RecipePreview(recipe: Recipe, previewUri: Bitmap?, onClick: () -> Unit) { +fun RecipePreview(entry: RecipeWithTags, previewUri: Bitmap?, onClick: () -> Unit) { Column(modifier = Modifier .padding(8.dp) .clickable(onClick = onClick)) { @@ -38,7 +39,7 @@ fun RecipePreview(recipe: Recipe, previewUri: Bitmap?, onClick: () -> Unit) { } Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) { Text( - text = recipe.title, + text = entry.recipe.title, modifier = Modifier.fillMaxWidth(), style = TextStyle( textAlign = TextAlign.Center, @@ -47,7 +48,7 @@ fun RecipePreview(recipe: Recipe, previewUri: Bitmap?, onClick: () -> Unit) { ) } Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) { - for (tag in recipe.tags) { + for (tag in entry.tags) { Tag(tag) } } diff --git a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/Tag.kt b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/Tag.kt index a74adb7..3e900dd 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/ui/components/Tag.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/ui/components/Tag.kt @@ -10,9 +10,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import xyz.pixelatedw.recipe.data.Tag @Composable -fun Tag(tag: String) { +fun Tag(tag: Tag) { Box( modifier = Modifier .padding(start = 8.dp) @@ -21,7 +22,7 @@ fun Tag(tag: String) { ) { Text( modifier = Modifier.padding(start = 8.dp, end = 8.dp), - text = tag + text = tag.name ) } } 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 b1df2ab..24efe15 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/utils/RecipeParser.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/utils/RecipeParser.kt @@ -1,61 +1,56 @@ 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 import io.github.wasabithumb.jtoml.JToml +import xyz.pixelatedw.recipe.data.AppDatabase import xyz.pixelatedw.recipe.data.Recipe -import xyz.pixelatedw.recipe.data.RecipesView +import xyz.pixelatedw.recipe.data.RecipeTag +import xyz.pixelatedw.recipe.data.Tag import java.io.BufferedReader +import java.io.File +import java.io.FileOutputStream import java.io.InputStreamReader -private val previews = HashMap() private val recipeFiles = mutableListOf() -fun getRecipes(ctx: Context, view: RecipesView, uri: Uri?) { +fun getRecipes(ctx: Context, db: AppDatabase, uri: Uri?) { if (uri == null) { return } + val path = "" + val dir = DocumentFile.fromTreeUri(ctx, uri) if (dir != null) { - parseDir(ctx, dir) + parseDir(ctx, dir, path) for (file in recipeFiles) { - val recipe = parseRecipe(ctx, file) - if (recipe != null) { - view.addRecipe(recipe) - } + parseRecipe(ctx, db, file) } } } -fun parseDir(ctx: Context, dir: DocumentFile) { +fun parseDir(ctx: Context, dir: DocumentFile, path: String) { val fileList: Array = dir.listFiles() for (file in fileList) { - if(file.isDirectory) { - parseDir(ctx, file) + if (file.isDirectory) { + parseDir(ctx, file, path + File.separator + file.name) continue; } if (file.isFile && file.name?.endsWith(".jpg") == true) { - val inStream = ctx.contentResolver.openInputStream(file.uri) + val picsDir = File(ctx.filesDir, path) + picsDir.mkdirs() - var bitmap: Bitmap? = null - if (file.exists()) { - try { - bitmap = BitmapFactory.decodeStream(inStream) - } - finally { - inStream?.close() - } - } + val newFile = File(picsDir, file.name!!) - if (bitmap != null) { - previews[file.name!!] = bitmap + ctx.contentResolver.openInputStream(file.uri).use { inStream -> + FileOutputStream(newFile, false).use { outStream -> + inStream?.copyTo(outStream) + } } } if (file.isFile && file.name?.endsWith(".md") == true) { @@ -64,7 +59,7 @@ fun parseDir(ctx: Context, dir: DocumentFile) { } } -private fun parseRecipe(ctx: Context, file: DocumentFile): Recipe? { +private fun parseRecipe(ctx: Context, db: AppDatabase, file: DocumentFile) { val lastModified = file.lastModified() val inputStream = ctx.contentResolver.openInputStream(file.uri) @@ -75,9 +70,8 @@ private fun parseRecipe(ctx: Context, file: DocumentFile): Recipe? { 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] + + for (line in lines) { frontMatterSize += line.trimmedLength() + 1 if (line == "+++") { @@ -91,38 +85,57 @@ private fun parseRecipe(ctx: Context, file: DocumentFile): Recipe? { sb.appendLine(line) } + if (!hasToml) { + reader.close() + return + } + val toml = JToml.jToml() val doc = toml.readFromString(sb.toString()) - val tags = arrayListOf() - for (tomlElem in doc["tags"]!!.asArray()) { - tags.add(tomlElem!!.asPrimitive().asString()) + if (!doc.contains("title")) { + reader.close() + return } + val recipeTitle = doc["title"]!!.asPrimitive().asString() + val pics = arrayListOf() - for (tomlElem in doc["pics"]!!.asArray()) { - pics.add(tomlElem!!.asPrimitive().asString()) + if (doc.contains("pics")) { + 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 tags = arrayListOf() + if (doc.contains("tags")) { + for (tomlElem in doc["tags"]!!.asArray()) { + val tag = Tag(tomlElem!!.asPrimitive().asString()) + tags.add(tag) + + val recipeWithTags = RecipeTag(recipeTitle, tag.name) + db.tagDao().insert(tag) + db.recipeWithTagsDao().insert(recipeWithTags) } } + var recipePreview: String? = null + for (pic in pics) { + recipePreview = pic + } + val content = text.substring(frontMatterSize..