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.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.room.Dao import androidx.room.Dao
import androidx.room.Delete import androidx.room.Delete
@ -18,6 +17,7 @@ import com.google.gson.reflect.TypeToken
import java.io.File import java.io.File
private val thumbnailsCache: HashMap<String, Bitmap> = hashMapOf()
private val imagesCache: HashMap<String, Bitmap> = hashMapOf() private val imagesCache: HashMap<String, Bitmap> = hashMapOf()
@Entity @Entity
@ -29,11 +29,29 @@ data class Recipe(
val content: String, val content: String,
val hash: Int val hash: Int
) { ) {
fun previewImage(ctx: Context, idx: Int): Bitmap? { fun getThumbnail(ctx: Context, idx: Int): Bitmap? {
return showImage(ctx, idx) 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) val img = this.pics.getOrNull(idx)
if (img != null) { if (img != null) {
val cachedImage = imagesCache[img] val cachedImage = imagesCache[img]
@ -53,6 +71,36 @@ data class Recipe(
return null 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) inline fun <reified T> Gson.fromJson(json: String?): T = fromJson<T>(json, object: TypeToken<T>() {}.type)
@ -71,6 +119,9 @@ class PicsTypeConvertor {
@Dao @Dao
interface RecipeDao { interface RecipeDao {
@Query("SELECT * FROM recipe")
fun getAll(): List<Recipe>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(recipe: Recipe) 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.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text 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.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -84,7 +80,7 @@ fun RecipeInfo(
contentPadding = PaddingValues(horizontal = 64.dp) contentPadding = PaddingValues(horizontal = 64.dp)
) { ) {
items(picsCounts) { idx -> items(picsCounts) { idx ->
val pic = active.recipe.showImage(ctx, idx) val pic = active.recipe.getFullImage(ctx, idx)
if (pic != null) { if (pic != null) {
Image( Image(
bitmap = pic.asImageBitmap(), 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.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.width
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
@ -78,7 +76,7 @@ fun RecipePreview(
.togetherWith(fadeOut(animationSpec = tween(500))) .togetherWith(fadeOut(animationSpec = tween(500)))
} }
) { targetImage -> ) { targetImage ->
val displayImage = entry.recipe.previewImage(LocalContext.current, targetImage) val displayImage = entry.recipe.getThumbnail(LocalContext.current, targetImage)
if (displayImage != null) { if (displayImage != null) {
Image( Image(
bitmap = displayImage.asImageBitmap(), bitmap = displayImage.asImageBitmap(),

View File

@ -13,6 +13,9 @@ import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.fileSize
private val recipeFiles = mutableListOf<DocumentFile>() private val recipeFiles = mutableListOf<DocumentFile>()
@ -50,6 +53,33 @@ fun parseRecipeFiles(ctx: MainActivity) {
ctx.db.tagDao().delete(tag.name) 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) { 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 val fileName = file.name ?: return
// println("file: ${file.name} | ${file.uri.path} | ${file.length()}")
if (fileName.endsWith(".jpg")) { if (fileName.endsWith(".jpg")) {
val picsDir = File(ctx.filesDir, path) val picsDir = File(ctx.filesDir, path)
@ -163,5 +192,7 @@ fun parseRecipe(ctx: MainActivity, lastModified: Long, text: String) {
hash = hash hash = hash
) )
recipe.generateThumbnails(ctx)
ctx.db.recipeDao().insert(recipe) ctx.db.recipeDao().insert(recipe)
} }