Compare commits

..

No commits in common. "d60cbe4678c809893cd1fe549a88b58daa015910" and "9131a028a3f5e684e7ac02cec0ca52d12e819a5f" have entirely different histories.

6 changed files with 100 additions and 95 deletions

View File

@ -3,8 +3,8 @@ package xyz.pixelatedw.recipe.data
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.widget.ImageView
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete import androidx.room.Delete
import androidx.room.Entity import androidx.room.Entity
@ -17,9 +17,6 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import java.io.File import java.io.File
private val imagesCache: HashMap<String, Bitmap> = hashMapOf()
@Entity @Entity
data class Recipe( data class Recipe(
@PrimaryKey @PrimaryKey
@ -36,17 +33,10 @@ data class Recipe(
fun showImage(ctx: Context, idx: Int): Bitmap? { fun showImage(ctx: Context, idx: Int): Bitmap? {
val img = this.pics.getOrNull(idx) val img = this.pics.getOrNull(idx)
if (img != null) { if (img != null) {
val cachedImage = imagesCache[img]
if (cachedImage != null) {
return cachedImage
}
val file = File(ctx.filesDir, img) val file = File(ctx.filesDir, img)
if (file.exists()) { if (file.exists()) {
ctx.contentResolver.openInputStream(file.toUri()).use { ctx.contentResolver.openInputStream(file.toUri()).use {
val bitmapData = BitmapFactory.decodeStream(it) return BitmapFactory.decodeStream(it)
imagesCache[img] = bitmapData
return bitmapData
} }
} }
} }

View File

@ -44,14 +44,20 @@ class RecipesView : ViewModel() {
} }
} }
val filters = filtersMap.map { TagFilter(it.key, checked = activeFilterNames.contains(it.key), count = it.value) } if (activeFilterNames.isNotEmpty()) {
_tagFilters.update { filters.distinct().sortedBy { it.tag }.toList() } _tagFilters.value = _tagFilters.value.map {
it.count = filtersMap[it.tag] ?: 0
it
}.toList()
} else {
val filters = filtersMap.map { TagFilter(it.key, count = it.value) }.toList()
_tagFilters.update { filters.distinct().sortedBy { it.tag } }
}
} }
fun setTagFilterState(tag: String, state: Boolean) { fun setTagFilterState(tag: String, state: Boolean) {
_tagFilters.value = _tagFilters.value.map { if (tag == it.tag) it.checked = state; it } _tagFilters.value = _tagFilters.value.toMutableList()
.apply { replaceAll { it -> if (tag == it.tag) it.checked = state; it } }.toList()
this.reloadTagFilterState()
} }
fun setKeepScreenOn(flag: Boolean) { fun setKeepScreenOn(flag: Boolean) {
@ -61,7 +67,19 @@ class RecipesView : ViewModel() {
fun setRecipes(recipes: List<RecipeWithTags>) { fun setRecipes(recipes: List<RecipeWithTags>) {
_recipes.update { recipes.sortedBy { it.recipe.title } } _recipes.update { recipes.sortedBy { it.recipe.title } }
this.reloadTagFilterState() val filtersMap = mutableMapOf<String, Int>()
_recipes.value.stream()
.filter { it.tags.isNotEmpty() }
.forEach {
it.tags.forEach { tag ->
val count = filtersMap[tag.name] ?: 0
filtersMap[tag.name] = count + 1;
}
}
val filters = filtersMap.map { it -> TagFilter(it.key, count = it.value) }.toList()
_tagFilters.update { filters.distinct().sortedBy { it.tag } }
} }
fun removeRecipe(recipe: RecipeWithTags) { fun removeRecipe(recipe: RecipeWithTags) {

View File

@ -4,12 +4,10 @@ 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.Row
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.lazy.grid.GridCells import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
@ -44,12 +42,12 @@ import java.io.File
@Composable @Composable
fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) { fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
val recipes by view.recipes.collectAsState() val recipes = view.recipes.collectAsState()
val active by view.activeRecipe.collectAsState() val active = view.activeRecipe.collectAsState()
val search by view.search.collectAsState() val search = view.search.collectAsState()
val filters by view.tagFilters.collectAsState() val filters = view.tagFilters.collectAsState()
var activeRecipes by remember { mutableStateOf(recipes) } var activeRecipes = remember { recipes.value }
var activeTags = 0 var activeTags = 0
val navController = rememberNavController() val navController = rememberNavController()
@ -58,8 +56,6 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
var openTagFilterDialog by remember { mutableStateOf(false) } var openTagFilterDialog by remember { mutableStateOf(false) }
var selectedEntries by remember { mutableStateOf(listOf<RecipeWithTags>()) } var selectedEntries by remember { mutableStateOf(listOf<RecipeWithTags>()) }
val gridState = rememberLazyGridState()
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = "list" startDestination = "list"
@ -77,11 +73,11 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
modifier = Modifier modifier = Modifier
.weight(0.7f) .weight(0.7f)
.padding(end = 4.dp), .padding(end = 4.dp),
value = search.orEmpty(), value = search.value.orEmpty(),
onValueChange = { search -> onValueChange = { search ->
view.setSearch(search) view.setSearch(search)
activeRecipes = activeRecipes =
recipes.filter { filterRecipe(it, search, filters) } recipes.value.filter { filterRecipe(it, search, filters.value) }
}, },
) )
// Tags / Delete // Tags / Delete
@ -133,34 +129,23 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
) )
} }
} }
LazyVerticalGrid( LazyColumn {
columns = GridCells.Fixed(3), items(activeRecipes) { entry ->
modifier = Modifier.fillMaxSize(), val isSelected = selectedEntries.contains(entry)
horizontalArrangement = Arrangement.spacedBy(8.dp), RecipePreview(entry, isSelected, onClick = {
verticalArrangement = Arrangement.spacedBy(8.dp), view.setActive(entry)
state = gridState, navController.navigate("info")
) { }, onSelected = { flag ->
items( selectedEntries =
count = activeRecipes.size, selectedEntries.toMutableList().apply {
key = { activeRecipes[it].recipe.title }, if (flag) {
itemContent = { entryId -> add(entry)
val entry = activeRecipes[entryId] } else {
val isSelected = selectedEntries.contains(entry) remove(entry)
RecipePreview(entry, isSelected, onClick = { }
view.setActive(entry) }.toList()
navController.navigate("info") })
}, onSelected = { flag -> }
selectedEntries =
selectedEntries.toMutableList().apply {
if (flag) {
add(entry)
} else {
remove(entry)
}
}.toList()
})
}
)
} }
} }
@ -193,12 +178,12 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
onAccept = { onAccept = {
openTagFilterDialog = false openTagFilterDialog = false
view.reloadTagFilterState() view.reloadTagFilterState()
activeTags = filters.count { f -> f.checked } activeTags = filters.value.count { f -> f.checked }
activeRecipes = recipes.filter { activeRecipes = recipes.value.filter {
filterRecipe( filterRecipe(
it, it,
search, search.value,
filters filters.value
) )
} }
}, },
@ -208,7 +193,7 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
} }
} }
composable("info") { composable("info") {
RecipeInfo(ctx, view, navController, padding, active!!) RecipeInfo(ctx, view, navController, padding, active.value!!)
} }
composable("settings") { composable("settings") {
SettingsScreen(ctx, navController) SettingsScreen(ctx, navController)

View File

@ -11,11 +11,10 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -23,7 +22,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
@ -34,6 +32,7 @@ import androidx.compose.ui.res.imageResource
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.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import xyz.pixelatedw.recipe.R import xyz.pixelatedw.recipe.R
import xyz.pixelatedw.recipe.data.RecipeWithTags import xyz.pixelatedw.recipe.data.RecipeWithTags
@ -59,20 +58,21 @@ fun RecipePreview(
Column( Column(
modifier = Modifier modifier = Modifier
.background(color = if (isSelected) Color(0x11FF0000) else Color(0x00FFFFFF)) .background(color = if (isSelected) Color(0x11FF0000) else Color(0x00FFFFFF))
.padding(8.dp)
.combinedClickable( .combinedClickable(
onLongClick = { onSelected(!isSelected) }, onLongClick = { onSelected(!isSelected) },
onClick = onClick onClick = onClick
) )
.height(192.dp)
) { ) {
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().weight(0.7f) modifier = Modifier
.fillMaxWidth()
.height(256.dp)
) { ) {
AnimatedContent( AnimatedContent(
displayImageId, displayImageId,
label = "Recipe Preview", label = "Recipe Preview",
modifier = Modifier.fillMaxSize(),
transitionSpec = { transitionSpec = {
fadeIn(animationSpec = tween(500)) fadeIn(animationSpec = tween(500))
.togetherWith(fadeOut(animationSpec = tween(500))) .togetherWith(fadeOut(animationSpec = tween(500)))
@ -83,39 +83,47 @@ fun RecipePreview(
Image( Image(
bitmap = displayImage.asImageBitmap(), bitmap = displayImage.asImageBitmap(),
contentDescription = "Recipe image", contentDescription = "Recipe image",
contentScale = ContentScale.FillHeight, contentScale = ContentScale.Crop,
modifier = Modifier
.size(256.dp)
.padding(top = 16.dp, bottom = 8.dp),
) )
} else { } else {
Image( Image(
bitmap = ImageBitmap.imageResource(R.drawable.missing_image), bitmap = ImageBitmap.imageResource(R.drawable.missing_image),
contentDescription = "Missing recipe image", contentDescription = "Missing recipe image",
contentScale = ContentScale.FillHeight, contentScale = ContentScale.Crop,
modifier = Modifier
.size(256.dp)
.padding(top = 16.dp, bottom = 8.dp),
) )
} }
} }
} }
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, modifier = Modifier
modifier = Modifier.fillMaxWidth().weight(0.2f) .fillMaxWidth()
.padding(bottom = 8.dp)
) { ) {
Text( Text(
text = entry.recipe.title, text = entry.recipe.title,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
style = TextStyle( style = TextStyle(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontSize = 7.em,
) )
) )
} }
// Row( Row(
// horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
// modifier = Modifier modifier = Modifier
// .weight(0.15f) .fillMaxWidth()
// .fillMaxWidth() .padding(bottom = 16.dp)
// ) { ) {
// for (tag in entry.tags) { for (tag in entry.tags) {
// Tag(tag) Tag(tag)
// } }
// } }
} }
} }

View File

@ -10,20 +10,18 @@ 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 androidx.compose.ui.unit.sp
import xyz.pixelatedw.recipe.data.Tag import xyz.pixelatedw.recipe.data.Tag
@Composable @Composable
fun Tag(tag: Tag) { fun Tag(tag: Tag) {
Box( Box(
modifier = Modifier modifier = Modifier
.padding(start = 4.dp) .padding(start = 8.dp)
.clip(RoundedCornerShape(percent = 25)) .clip(RoundedCornerShape(percent = 50))
.background(Color(0, 153, 170)) .background(Color(0, 153, 170))
) { ) {
Text( Text(
modifier = Modifier.padding(start = 4.dp, end = 4.dp), modifier = Modifier.padding(start = 8.dp, end = 8.dp),
fontSize = 12.sp,
text = tag.name text = tag.name
) )
} }

View File

@ -16,23 +16,20 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
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 androidx.compose.ui.unit.em import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import xyz.pixelatedw.recipe.data.RecipesView import xyz.pixelatedw.recipe.data.RecipesView
@Composable @Composable
fun TagFilterDialog(onAccept: () -> Unit, view: RecipesView) { fun TagFilterDialog(onAccept: () -> Unit, view: RecipesView) {
val filters by view.tagFilters.collectAsState() val filters = view.tagFilters.collectAsState()
Dialog(onDismissRequest = { }) { Dialog(onDismissRequest = { }) {
Card( Card(
@ -49,11 +46,19 @@ fun TagFilterDialog(onAccept: () -> Unit, view: RecipesView) {
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.wrapContentSize(Alignment.TopStart) .wrapContentSize(Alignment.TopStart)
) { ) {
items(filters) { tag -> items(filters.value) { tag ->
if (tag.count <= 0) {
return@items
}
// TODO This doesn't really feel right lmao, but for now it works
val filterChecked = remember { mutableStateOf(tag.checked) }
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Checkbox(checked = tag.checked, onCheckedChange = { Checkbox(checked = filterChecked.value, onCheckedChange = {
filterChecked.value = !tag.checked
view.setTagFilterState(tag.tag, !tag.checked) view.setTagFilterState(tag.tag, !tag.checked)
}) })
TextButton( TextButton(
@ -63,6 +68,7 @@ fun TagFilterDialog(onAccept: () -> Unit, view: RecipesView) {
containerColor = Color.Transparent containerColor = Color.Transparent
), ),
onClick = { onClick = {
filterChecked.value = !tag.checked
view.setTagFilterState(tag.tag, !tag.checked) view.setTagFilterState(tag.tag, !tag.checked)
} }
) { ) {