From 143555c22e59cad79685d38d234d7ae3c28dafbb Mon Sep 17 00:00:00 2001 From: Wynd Date: Fri, 13 Mar 2026 15:08:13 +0200 Subject: [PATCH] Generate small thumbnails for recipe previews and delete unused pics on import/syncs --- .../java/xyz/pixelatedw/recipe/data/Recipe.kt | 59 +++++++++++++++++-- .../recipe/ui/components/RecipeInfo.kt | 6 +- .../recipe/ui/components/RecipePreview.kt | 4 +- .../pixelatedw/recipe/utils/RecipeParser.kt | 33 ++++++++++- 4 files changed, 89 insertions(+), 13 deletions(-) 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 5eec5bb..40820d6 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/data/Recipe.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/data/Recipe.kt @@ -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 = hashMapOf() private val imagesCache: HashMap = 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 + } + + 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 showImage(ctx: Context, idx: Int): Bitmap? { + 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 Gson.fromJson(json: String?): T = fromJson(json, object: TypeToken() {}.type) @@ -71,6 +119,9 @@ class PicsTypeConvertor { @Dao interface RecipeDao { + @Query("SELECT * FROM recipe") + fun getAll(): List + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(recipe: Recipe) 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 be178df..df0b4de 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 @@ -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(), 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 3ba665d..0420514 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 @@ -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(), 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 a16b945..87f03ad 100644 --- a/app/src/main/java/xyz/pixelatedw/recipe/utils/RecipeParser.kt +++ b/app/src/main/java/xyz/pixelatedw/recipe/utils/RecipeParser.kt @@ -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() @@ -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() + + 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) }