Generate small thumbnails for recipe previews and delete unused pics on import/syncs

master
Wynd 2026-03-13 15:08:13 +02:00
parent 6dbd014519
commit 143555c22e
4 changed files with 89 additions and 13 deletions

View File

@ -3,7 +3,6 @@ package xyz.pixelatedw.recipe.data
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.widget.ImageView
import androidx.core.net.toUri
import androidx.room.Dao
import androidx.room.Delete
@ -18,6 +17,7 @@ import com.google.gson.reflect.TypeToken
import java.io.File
private val thumbnailsCache: HashMap<String, Bitmap> = hashMapOf()
private val imagesCache: HashMap<String, Bitmap> = hashMapOf()
@Entity
@ -29,11 +29,29 @@ data class Recipe(
val content: String,
val hash: Int
) {
fun previewImage(ctx: Context, idx: Int): Bitmap? {
return showImage(ctx, idx)
fun getThumbnail(ctx: Context, idx: Int): Bitmap? {
val img = this.pics.getOrNull(idx)
if (img != null) {
val cachedImage = thumbnailsCache[img]
if (cachedImage != null) {
return cachedImage
}
fun showImage(ctx: Context, idx: Int): Bitmap? {
val originalFile = File(ctx.filesDir, img)
val thumbnail = File(originalFile.parentFile, "thumb-${originalFile.name}")
if (thumbnail.exists()) {
ctx.contentResolver.openInputStream(thumbnail.toUri()).use {
val bitmapData = BitmapFactory.decodeStream(it)
thumbnailsCache[img] = bitmapData
return bitmapData
}
}
}
return null
}
fun getFullImage(ctx: Context, idx: Int): Bitmap? {
val img = this.pics.getOrNull(idx)
if (img != null) {
val cachedImage = imagesCache[img]
@ -53,6 +71,36 @@ data class Recipe(
return null
}
fun generateThumbnails(ctx: Context) {
for (img in this.pics) {
val file = File(ctx.filesDir, img)
ctx.contentResolver.openInputStream(file.toUri()).use {
var bitmapData = BitmapFactory.decodeStream(it)
val aspectRatio: Float = bitmapData.getWidth() / bitmapData.getHeight().toFloat()
val width = 256
val height = Math.round(width / aspectRatio)
bitmapData = Bitmap.createScaledBitmap(bitmapData, width, height, true);
val thumbnailName = "thumb-${file.name}"
val thumbnailFile = File(file.parentFile, thumbnailName)
if (!thumbnailFile.exists()) {
thumbnailFile.createNewFile();
}
ctx.contentResolver.openOutputStream(thumbnailFile.toUri()).use { out ->
bitmapData.compress(Bitmap.CompressFormat.PNG, 100, out!!)
}
}
}
}
companion object {
fun clearCache() {
thumbnailsCache.clear()
imagesCache.clear()
}
}
}
inline fun <reified T> Gson.fromJson(json: String?): T = fromJson<T>(json, object: TypeToken<T>() {}.type)
@ -71,6 +119,9 @@ class PicsTypeConvertor {
@Dao
interface RecipeDao {
@Query("SELECT * FROM recipe")
fun getAll(): List<Recipe>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(recipe: Recipe)

View File

@ -17,17 +17,13 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
@ -84,7 +80,7 @@ fun RecipeInfo(
contentPadding = PaddingValues(horizontal = 64.dp)
) {
items(picsCounts) { idx ->
val pic = active.recipe.showImage(ctx, idx)
val pic = active.recipe.getFullImage(ctx, idx)
if (pic != null) {
Image(
bitmap = pic.asImageBitmap(),

View File

@ -14,8 +14,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -78,7 +76,7 @@ fun RecipePreview(
.togetherWith(fadeOut(animationSpec = tween(500)))
}
) { targetImage ->
val displayImage = entry.recipe.previewImage(LocalContext.current, targetImage)
val displayImage = entry.recipe.getThumbnail(LocalContext.current, targetImage)
if (displayImage != null) {
Image(
bitmap = displayImage.asImageBitmap(),

View File

@ -13,6 +13,9 @@ import java.io.BufferedReader
import java.io.File
import java.io.FileOutputStream
import java.io.InputStreamReader
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.fileSize
private val recipeFiles = mutableListOf<DocumentFile>()
@ -50,6 +53,33 @@ fun parseRecipeFiles(ctx: MainActivity) {
ctx.db.tagDao().delete(tag.name)
}
}
// Generate a list with all the pics used by all the registered recipes
val usedPics = ctx.db.recipeDao().getAll()
.asSequence()
.flatMap { it.pics }
.map { File(it) }
.map { p ->
val list = mutableListOf<String>()
list.add(p.name)
list.add("thumb-${p.name}")
list
}
.flatten()
.toList()
// Clear unused images
ctx.filesDir.walkTopDown().forEach {
if (it.name.endsWith(".jpg") && !usedPics.contains(it.name)) {
it.delete()
}
}
Recipe.clearCache()
// ctx.filesDir.walkTopDown().forEach { println(it.absolutePath + " | " + Path(it.absolutePath).fileSize()) }
}
fun parseDir(ctx: Context, dir: DocumentFile, path: String) {
@ -70,7 +100,6 @@ fun handleFile(ctx: Context, file: DocumentFile, path: String) {
}
val fileName = file.name ?: return
// println("file: ${file.name} | ${file.uri.path} | ${file.length()}")
if (fileName.endsWith(".jpg")) {
val picsDir = File(ctx.filesDir, path)
@ -163,5 +192,7 @@ fun parseRecipe(ctx: MainActivity, lastModified: Long, text: String) {
hash = hash
)
recipe.generateThumbnails(ctx)
ctx.db.recipeDao().insert(recipe)
}