Compare commits

..

No commits in common. "143555c22e59cad79685d38d234d7ae3c28dafbb" and "d60cbe4678c809893cd1fe549a88b58daa015910" have entirely different histories.

11 changed files with 73 additions and 182 deletions

1
app/.gitignore vendored
View File

@ -1,2 +1 @@
/build
/release

View File

@ -13,8 +13,8 @@ android {
applicationId = "xyz.pixelatedw.recipe"
minSdk = 24
targetSdk = 34
versionCode = 2
versionName = "1.1"
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@ -28,24 +28,16 @@ android {
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
lint {
checkReleaseBuilds = false
abortOnError = false
}
}
dependencies {

View File

@ -11,10 +11,10 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.room.Room
import xyz.pixelatedw.recipe.ui.theme.RecipeTheme
import xyz.pixelatedw.recipe.data.AppDatabase
import xyz.pixelatedw.recipe.data.RecipesView
import xyz.pixelatedw.recipe.ui.components.MainScreen
import xyz.pixelatedw.recipe.ui.theme.RecipeTheme
import xyz.pixelatedw.recipe.utils.parseRecipes
class MainActivity : ComponentActivity() {
@ -54,7 +54,8 @@ class MainActivity : ComponentActivity() {
result.data?.data?.let { uri ->
parseRecipes(this, uri)
}
} else {
}
else {
importError = "Import cancelled"
}

View File

@ -3,6 +3,7 @@ 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
@ -17,7 +18,6 @@ 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,29 +29,11 @@ data class Recipe(
val content: String,
val hash: Int
) {
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 previewImage(ctx: Context, idx: Int): Bitmap? {
return showImage(ctx, idx)
}
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? {
fun showImage(ctx: Context, idx: Int): Bitmap? {
val img = this.pics.getOrNull(idx)
if (img != null) {
val cachedImage = imagesCache[img]
@ -71,36 +53,6 @@ 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)
@ -119,9 +71,6 @@ class PicsTypeConvertor {
@Dao
interface RecipeDao {
@Query("SELECT * FROM recipe")
fun getAll(): List<Recipe>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(recipe: Recipe)

View File

@ -37,6 +37,7 @@ data class RecipeWithTags(
@Dao
interface RecipeWithTagsDao {
@Transaction
@Query("SELECT * FROM recipe")
fun getAll(): List<RecipeWithTags>
@ -46,7 +47,6 @@ interface RecipeWithTagsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(recipe: RecipeTag)
@Transaction
@Query("DELETE FROM recipetag WHERE recipetag.title = :recipe")
fun delete(recipe: String)
}

View File

@ -5,8 +5,6 @@ import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Transaction
@Entity
data class Tag(
@ -16,16 +14,6 @@ data class Tag(
@Dao
interface TagDao {
@Query("SELECT * FROM tag")
fun getAll(): List<Tag>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(tag: Tag)
@Query("SELECT COUNT(*) FROM tag")
fun count(): Int
@Transaction
@Query("DELETE FROM tag WHERE tag.name = :name")
fun delete(name: String)
}

View File

@ -49,6 +49,7 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
val search by view.search.collectAsState()
val filters by view.tagFilters.collectAsState()
var activeRecipes by remember { mutableStateOf(recipes) }
var activeTags = 0
val navController = rememberNavController()
@ -79,9 +80,8 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
value = search.orEmpty(),
onValueChange = { search ->
view.setSearch(search)
val newRecipes =
activeRecipes =
recipes.filter { filterRecipe(it, search, filters) }
view.setRecipes(newRecipes)
},
)
// Tags / Delete
@ -141,10 +141,10 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
state = gridState,
) {
items(
count = recipes.size,
key = { recipes[it].recipe.title },
count = activeRecipes.size,
key = { activeRecipes[it].recipe.title },
itemContent = { entryId ->
val entry = recipes[entryId]
val entry = activeRecipes[entryId]
val isSelected = selectedEntries.contains(entry)
RecipePreview(entry, isSelected, onClick = {
view.setActive(entry)
@ -194,14 +194,13 @@ fun MainScreen(ctx: MainActivity, padding: PaddingValues, view: RecipesView) {
openTagFilterDialog = false
view.reloadTagFilterState()
activeTags = filters.count { f -> f.checked }
val newRecipes = recipes.filter {
activeRecipes = recipes.filter {
filterRecipe(
it,
search,
filters
)
}
ctx.recipeView.setRecipes(newRecipes)
},
view,
)

View File

@ -17,13 +17,17 @@ 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
@ -80,7 +84,7 @@ fun RecipeInfo(
contentPadding = PaddingValues(horizontal = 64.dp)
) {
items(picsCounts) { idx ->
val pic = active.recipe.getFullImage(ctx, idx)
val pic = active.recipe.showImage(ctx, idx)
if (pic != null) {
Image(
bitmap = pic.asImageBitmap(),

View File

@ -14,6 +14,8 @@ 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
@ -76,7 +78,7 @@ fun RecipePreview(
.togetherWith(fadeOut(animationSpec = tween(500)))
}
) { targetImage ->
val displayImage = entry.recipe.getThumbnail(LocalContext.current, targetImage)
val displayImage = entry.recipe.previewImage(LocalContext.current, targetImage)
if (displayImage != null) {
Image(
bitmap = displayImage.asImageBitmap(),

View File

@ -13,9 +13,6 @@ 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>()
@ -45,41 +42,6 @@ fun parseRecipeFiles(ctx: MainActivity) {
reader.close()
}
// Clearing unused tags after an import/sync
val usedTags = ctx.recipeView.tagFilters.value.map { t -> t.tag }
for (tag in ctx.db.tagDao().getAll()) {
if (!usedTags.contains(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) {
@ -100,6 +62,7 @@ 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)
@ -192,7 +155,5 @@ fun parseRecipe(ctx: MainActivity, lastModified: Long, text: String) {
hash = hash
)
recipe.generateThumbnails(ctx)
ctx.db.recipeDao().insert(recipe)
}

View File

@ -59,11 +59,11 @@ fun sync(ctx: MainActivity, nav: NavHostController, setLoading: (Boolean) -> Uni
inputStream.read(buffer)
val contentBufLen = ByteBuffer.wrap(buffer).getLong().toInt()
var parentDir = File(ctx.filesDir, ".sync")
var parentDir = ctx.filesDir
if (filePath.isNotEmpty()) {
parentDir = File(ctx.filesDir, filePath)
}
parentDir.mkdirs()
}
val newFile = File(parentDir, fileName)
@ -94,10 +94,6 @@ fun sync(ctx: MainActivity, nav: NavHostController, setLoading: (Boolean) -> Uni
parseDir(ctx, docDir, "")
parseRecipeFiles(ctx)
val syncFolder = File(ctx.filesDir, ".sync")
if (syncFolder.exists()) {
syncFolder.deleteRecursively()
}
} catch (e: Exception) {
ctx.importError = when (e) {
is SocketTimeoutException -> "Connection timed out"