Generate small thumbnails for recipe previews and delete unused pics on import/syncs
parent
6dbd014519
commit
143555c22e
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue