Caching loaded recipes in database, copying files to app storage for easier handling and some fixes
parent
0af796082c
commit
0b2e301f8a
|
@ -2,6 +2,7 @@ plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
alias(libs.plugins.devtools.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -49,18 +50,22 @@ dependencies {
|
||||||
implementation(libs.androidx.ui.tooling.preview)
|
implementation(libs.androidx.ui.tooling.preview)
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.androidx.documentfile)
|
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)
|
testImplementation(libs.junit)
|
||||||
|
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
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.androidx.navigation.fragment)
|
|
||||||
implementation(libs.androidx.navigation.navigation.ui)
|
|
||||||
implementation(libs.androidx.navigation.navigation.compose)
|
|
||||||
|
|
||||||
implementation(libs.jtoml)
|
|
||||||
implementation(libs.commonmark)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package xyz.pixelatedw.recipe
|
package xyz.pixelatedw.recipe
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
@ -11,6 +10,8 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.ui.Modifier
|
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.data.RecipesView
|
||||||
import xyz.pixelatedw.recipe.ui.components.MainScreen
|
import xyz.pixelatedw.recipe.ui.components.MainScreen
|
||||||
import xyz.pixelatedw.recipe.ui.theme.RecipeTheme
|
import xyz.pixelatedw.recipe.ui.theme.RecipeTheme
|
||||||
|
@ -18,11 +19,18 @@ import xyz.pixelatedw.recipe.utils.getRecipes
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val recipeView: RecipesView by viewModels()
|
private val recipeView: RecipesView by viewModels()
|
||||||
|
private lateinit var db: AppDatabase
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
findSourceDir()
|
db = Room.databaseBuilder(
|
||||||
|
this,
|
||||||
|
AppDatabase::class.java, "recipes"
|
||||||
|
).allowMainThreadQueries().build()
|
||||||
|
|
||||||
|
val recipes = db.recipeWithTagsDao().getAll()
|
||||||
|
recipeView.setRecipes(recipes)
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
@ -30,25 +38,22 @@ class MainActivity : ComponentActivity() {
|
||||||
RecipeTheme {
|
RecipeTheme {
|
||||||
Surface {
|
Surface {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||||
MainScreen(innerPadding, recipeView)
|
MainScreen(this, innerPadding, recipeView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findSourceDir() {
|
val sourceChooser =
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == RESULT_OK) {
|
||||||
|
result.data?.data?.let { uri ->
|
||||||
|
getRecipes(this, db, uri)
|
||||||
|
|
||||||
val getContent =
|
val recipes = db.recipeWithTagsDao().getAll()
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
recipeView.setRecipes(recipes)
|
||||||
if (result.resultCode == RESULT_OK) {
|
|
||||||
result.data?.data?.let { uri ->
|
|
||||||
getRecipes(this, recipeView, uri)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
getContent.launch(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,15 +1,41 @@
|
||||||
package xyz.pixelatedw.recipe.data
|
package xyz.pixelatedw.recipe.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
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(
|
data class Recipe(
|
||||||
|
@PrimaryKey
|
||||||
val title: String,
|
val title: String,
|
||||||
val tags: List<String>,
|
val preview: String?,
|
||||||
val previews: List<Bitmap>,
|
|
||||||
val lastModified: Long,
|
val lastModified: Long,
|
||||||
val content: String
|
val content: String
|
||||||
) {
|
) {
|
||||||
fun mainImage(): Bitmap? {
|
fun mainImage(ctx: Context): Bitmap? {
|
||||||
return this.previews.getOrNull(0)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -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<Tag>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface RecipeWithTagsDao {
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM recipe")
|
||||||
|
fun getAll(): List<RecipeWithTags>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insert(recipe: RecipeTag)
|
||||||
|
}
|
|
@ -6,26 +6,24 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
class RecipesView : ViewModel() {
|
class RecipesView : ViewModel() {
|
||||||
private val _activeRecipe = MutableStateFlow<Recipe?>( null )
|
private val _activeRecipe = MutableStateFlow<RecipeWithTags?>( null )
|
||||||
val activeRecipe = _activeRecipe.asStateFlow()
|
val activeRecipe = _activeRecipe.asStateFlow()
|
||||||
|
|
||||||
private val _recipes = MutableStateFlow<List<Recipe>>( arrayListOf() )
|
private val _recipes = MutableStateFlow<List<RecipeWithTags>>( arrayListOf() )
|
||||||
val recipes = _recipes.asStateFlow()
|
val recipes = _recipes.asStateFlow()
|
||||||
|
|
||||||
private val _search = MutableStateFlow<String?>(null)
|
private val _search = MutableStateFlow<String?>(null)
|
||||||
val search = _search.asStateFlow()
|
val search = _search.asStateFlow()
|
||||||
|
|
||||||
|
fun setRecipes(recipes: List<RecipeWithTags>) {
|
||||||
|
_recipes.update { recipes }
|
||||||
|
}
|
||||||
|
|
||||||
fun setSearch(search: String) {
|
fun setSearch(search: String) {
|
||||||
_search.update { search }
|
_search.update { search }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addRecipe(recipe: Recipe) {
|
fun setActive(recipe: RecipeWithTags) {
|
||||||
_recipes.update {
|
|
||||||
it + recipe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setActive(recipe: Recipe) {
|
|
||||||
_activeRecipe.update { recipe }
|
_activeRecipe.update { recipe }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -1,34 +1,44 @@
|
||||||
package xyz.pixelatedw.recipe.ui.components
|
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.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
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.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.compose.ui.unit.dp
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
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
|
import xyz.pixelatedw.recipe.data.RecipesView
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(padding: PaddingValues, view: RecipesView) {
|
fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
|
||||||
val recipes = view.recipes.collectAsState()
|
val recipes = view.recipes.collectAsState()
|
||||||
val active = view.activeRecipe.collectAsState()
|
val active = view.activeRecipe.collectAsState()
|
||||||
val search = view.search.collectAsState()
|
val search = view.search.collectAsState()
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
val isInSearch = isInSearch@{ recipe: Recipe ->
|
val isInSearch = isInSearch@{ entry: RecipeWithTags ->
|
||||||
val hasTitle = recipe.title.contains(search.value.orEmpty(), ignoreCase = true)
|
val hasTitle = entry.recipe.title.contains(search.value.orEmpty(), ignoreCase = true)
|
||||||
val hasTags = recipe.tags.isNotEmpty() && recipe.tags.stream()
|
val hasTags = entry.tags.isNotEmpty() && entry.tags.stream()
|
||||||
.filter { tag -> tag.contains(search.value.orEmpty(), ignoreCase = true) }.count() > 0
|
.filter { tag -> tag.name.contains(search.value.orEmpty(), ignoreCase = true) }
|
||||||
|
.count() > 0
|
||||||
|
|
||||||
hasTitle || hasTags
|
hasTitle || hasTags
|
||||||
}
|
}
|
||||||
|
@ -39,18 +49,33 @@ fun MainScreen(padding: PaddingValues, view: RecipesView) {
|
||||||
) {
|
) {
|
||||||
composable("list") {
|
composable("list") {
|
||||||
Column(modifier = Modifier.padding(padding)) {
|
Column(modifier = Modifier.padding(padding)) {
|
||||||
OutlinedTextField(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
value = search.value.orEmpty(),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
onValueChange = { search -> view.setSearch(search) })
|
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 {
|
LazyColumn {
|
||||||
items(recipes.value) { recipe ->
|
items(recipes.value) { entry ->
|
||||||
if (isInSearch(recipe)) {
|
if (isInSearch(entry)) {
|
||||||
val previewUri = recipe.mainImage()
|
val previewUri = entry.recipe.mainImage(LocalContext.current)
|
||||||
RecipePreview(recipe, previewUri, onClick = {
|
RecipePreview(entry, previewUri, onClick = {
|
||||||
view.setActive(recipe)
|
view.setActive(entry)
|
||||||
navController.navigate("info")
|
navController.navigate("info")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,11 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import xyz.pixelatedw.recipe.data.Recipe
|
import xyz.pixelatedw.recipe.data.Recipe
|
||||||
|
import xyz.pixelatedw.recipe.data.RecipeWithTags
|
||||||
import xyz.pixelatedw.recipe.utils.parseMarkdown
|
import xyz.pixelatedw.recipe.utils.parseMarkdown
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RecipeInfo(padding: PaddingValues, active: Recipe) {
|
fun RecipeInfo(padding: PaddingValues, active: RecipeWithTags) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
|
@ -30,7 +31,7 @@ fun RecipeInfo(padding: PaddingValues, active: Recipe) {
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = active.title,
|
text = active.recipe.title,
|
||||||
style = MaterialTheme.typography.displayLarge,
|
style = MaterialTheme.typography.displayLarge,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
@ -40,7 +41,7 @@ fun RecipeInfo(padding: PaddingValues, active: Recipe) {
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
val annotatedString =
|
val annotatedString =
|
||||||
parseMarkdown(active.content, MaterialTheme.typography)
|
parseMarkdown(active.recipe.content, MaterialTheme.typography)
|
||||||
Text(
|
Text(
|
||||||
text = annotatedString,
|
text = annotatedString,
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
|
|
|
@ -20,9 +20,10 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
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 xyz.pixelatedw.recipe.data.Recipe
|
import xyz.pixelatedw.recipe.data.Recipe
|
||||||
|
import xyz.pixelatedw.recipe.data.RecipeWithTags
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RecipePreview(recipe: Recipe, previewUri: Bitmap?, onClick: () -> Unit) {
|
fun RecipePreview(entry: RecipeWithTags, previewUri: Bitmap?, onClick: () -> Unit) {
|
||||||
Column(modifier = Modifier
|
Column(modifier = Modifier
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.clickable(onClick = onClick)) {
|
.clickable(onClick = onClick)) {
|
||||||
|
@ -38,7 +39,7 @@ fun RecipePreview(recipe: Recipe, previewUri: Bitmap?, onClick: () -> Unit) {
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
text = recipe.title,
|
text = entry.recipe.title,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
textAlign = TextAlign.Center,
|
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)) {
|
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) {
|
||||||
for (tag in recipe.tags) {
|
for (tag in entry.tags) {
|
||||||
Tag(tag)
|
Tag(tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,10 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import xyz.pixelatedw.recipe.data.Tag
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Tag(tag: String) {
|
fun Tag(tag: Tag) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 8.dp)
|
.padding(start = 8.dp)
|
||||||
|
@ -21,7 +22,7 @@ fun Tag(tag: String) {
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
|
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
|
||||||
text = tag
|
text = tag.name
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +1,56 @@
|
||||||
package xyz.pixelatedw.recipe.utils
|
package xyz.pixelatedw.recipe.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.text.trimmedLength
|
import androidx.core.text.trimmedLength
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import io.github.wasabithumb.jtoml.JToml
|
import io.github.wasabithumb.jtoml.JToml
|
||||||
|
import xyz.pixelatedw.recipe.data.AppDatabase
|
||||||
import xyz.pixelatedw.recipe.data.Recipe
|
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.BufferedReader
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
private val previews = HashMap<String, Bitmap>()
|
|
||||||
private val recipeFiles = mutableListOf<DocumentFile>()
|
private val recipeFiles = mutableListOf<DocumentFile>()
|
||||||
|
|
||||||
fun getRecipes(ctx: Context, view: RecipesView, uri: Uri?) {
|
fun getRecipes(ctx: Context, db: AppDatabase, uri: Uri?) {
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val path = ""
|
||||||
|
|
||||||
val dir = DocumentFile.fromTreeUri(ctx, uri)
|
val dir = DocumentFile.fromTreeUri(ctx, uri)
|
||||||
if (dir != null) {
|
if (dir != null) {
|
||||||
parseDir(ctx, dir)
|
parseDir(ctx, dir, path)
|
||||||
|
|
||||||
for (file in recipeFiles) {
|
for (file in recipeFiles) {
|
||||||
val recipe = parseRecipe(ctx, file)
|
parseRecipe(ctx, db, file)
|
||||||
if (recipe != null) {
|
|
||||||
view.addRecipe(recipe)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseDir(ctx: Context, dir: DocumentFile) {
|
fun parseDir(ctx: Context, dir: DocumentFile, path: String) {
|
||||||
val fileList: Array<DocumentFile> = dir.listFiles()
|
val fileList: Array<DocumentFile> = dir.listFiles()
|
||||||
for (file in fileList) {
|
for (file in fileList) {
|
||||||
if(file.isDirectory) {
|
if (file.isDirectory) {
|
||||||
parseDir(ctx, file)
|
parseDir(ctx, file, path + File.separator + file.name)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.isFile && file.name?.endsWith(".jpg") == true) {
|
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
|
val newFile = File(picsDir, file.name!!)
|
||||||
if (file.exists()) {
|
|
||||||
try {
|
|
||||||
bitmap = BitmapFactory.decodeStream(inStream)
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
inStream?.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bitmap != null) {
|
ctx.contentResolver.openInputStream(file.uri).use { inStream ->
|
||||||
previews[file.name!!] = bitmap
|
FileOutputStream(newFile, false).use { outStream ->
|
||||||
|
inStream?.copyTo(outStream)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (file.isFile && file.name?.endsWith(".md") == true) {
|
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 lastModified = file.lastModified()
|
||||||
|
|
||||||
val inputStream = ctx.contentResolver.openInputStream(file.uri)
|
val inputStream = ctx.contentResolver.openInputStream(file.uri)
|
||||||
|
@ -75,9 +70,8 @@ private fun parseRecipe(ctx: Context, file: DocumentFile): Recipe? {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
var hasToml = false
|
var hasToml = false
|
||||||
var frontMatterSize = 0
|
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) {
|
for (line in lines) {
|
||||||
val line = lines[i]
|
|
||||||
frontMatterSize += line.trimmedLength() + 1
|
frontMatterSize += line.trimmedLength() + 1
|
||||||
|
|
||||||
if (line == "+++") {
|
if (line == "+++") {
|
||||||
|
@ -91,38 +85,57 @@ private fun parseRecipe(ctx: Context, file: DocumentFile): Recipe? {
|
||||||
sb.appendLine(line)
|
sb.appendLine(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasToml) {
|
||||||
|
reader.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val toml = JToml.jToml()
|
val toml = JToml.jToml()
|
||||||
val doc = toml.readFromString(sb.toString())
|
val doc = toml.readFromString(sb.toString())
|
||||||
|
|
||||||
val tags = arrayListOf<String>()
|
if (!doc.contains("title")) {
|
||||||
for (tomlElem in doc["tags"]!!.asArray()) {
|
reader.close()
|
||||||
tags.add(tomlElem!!.asPrimitive().asString())
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val recipeTitle = doc["title"]!!.asPrimitive().asString()
|
||||||
|
|
||||||
val pics = arrayListOf<String>()
|
val pics = arrayListOf<String>()
|
||||||
for (tomlElem in doc["pics"]!!.asArray()) {
|
if (doc.contains("pics")) {
|
||||||
pics.add(tomlElem!!.asPrimitive().asString())
|
for (tomlElem in doc["pics"]!!.asArray()) {
|
||||||
|
pics.add(tomlElem!!.asPrimitive().asString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val recipePreviews = mutableListOf<Bitmap>()
|
val tags = arrayListOf<Tag>()
|
||||||
for (pic in pics) {
|
if (doc.contains("tags")) {
|
||||||
val bitmap = previews[pic]
|
for (tomlElem in doc["tags"]!!.asArray()) {
|
||||||
if (bitmap != null) {
|
val tag = Tag(tomlElem!!.asPrimitive().asString())
|
||||||
recipePreviews.add(bitmap)
|
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..<text.length)
|
val content = text.substring(frontMatterSize..<text.length)
|
||||||
|
|
||||||
val recipe = Recipe(
|
val recipe = Recipe(
|
||||||
title = doc["title"]!!.asPrimitive().asString(),
|
title = recipeTitle,
|
||||||
tags = tags,
|
preview = recipePreview,
|
||||||
previews = recipePreviews,
|
|
||||||
lastModified = lastModified,
|
lastModified = lastModified,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
|
|
||||||
reader.close()
|
db.recipeDao().insert(recipe)
|
||||||
|
|
||||||
return recipe
|
println(recipe)
|
||||||
|
|
||||||
|
reader.close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,5 @@ plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
alias(libs.plugins.kotlin.compose) apply false
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
|
alias(libs.plugins.devtools.ksp) apply false
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,13 @@ composeBom = "2025.07.00"
|
||||||
documentfile = "1.1.0"
|
documentfile = "1.1.0"
|
||||||
commonmark = "0.25.1"
|
commonmark = "0.25.1"
|
||||||
navVersion = "2.9.3"
|
navVersion = "2.9.3"
|
||||||
|
room = "2.7.2"
|
||||||
|
roomCompiler = "2.7.2"
|
||||||
|
ksp = "2.0.21-1.0.27"
|
||||||
|
|
||||||
[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" }
|
||||||
|
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" }
|
||||||
jtoml = { group = "io.github.wasabithumb", name = "jtoml", version.ref = "jtoml" }
|
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" }
|
||||||
|
@ -34,9 +38,10 @@ commonmark = { group = "org.commonmark", name = "commonmark", version.ref = "com
|
||||||
androidx-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navVersion"}
|
androidx-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navVersion"}
|
||||||
androidx-navigation-navigation-ui = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navVersion"}
|
androidx-navigation-navigation-ui = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navVersion"}
|
||||||
androidx-navigation-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navVersion"}
|
androidx-navigation-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navVersion"}
|
||||||
|
androidx-room = { group = "androidx.room", name = "room-runtime", version.ref = "room"}
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"}
|
||||||
|
|
Loading…
Reference in New Issue