From 4e20fd1cd6fcb00703288dff101a1386fa64845b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com> Date: Fri, 19 Apr 2024 09:19:31 -0400 Subject: [PATCH 1/3] Adjustment in proguard and yaml for release --- .github/workflows/cd-android.yml | 25 +++++++++++++-------- .github/workflows/ci-android.yaml | 6 +++--- app/proguard-rules.pro | 36 +++++++++++++++++++++++++------ 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/.github/workflows/cd-android.yml b/.github/workflows/cd-android.yml index 7864cef..ff92d86 100644 --- a/.github/workflows/cd-android.yml +++ b/.github/workflows/cd-android.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' @@ -32,7 +32,7 @@ jobs: - name: Generate file name env var run: | - DATE=$(date +'%d.%m.%Y') + DATE=$(date +'%d.%m.%Y-%H%M%S') BRANCH_NAME=${GITHUB_REF##*/} MESSAGE=$(cat << EOF PurrfectPics-release-${BRANCH_NAME}-${DATE} @@ -47,9 +47,9 @@ jobs: mv "./app/build/outputs/apk/release/app-release.apk" "./app/build/outputs/apk/release/${{ env.OUTPUT_NAME }}.apk" - name: Upload release APK - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: - name: ${{ env.OUTPUT_NAME }} + name: ${{ env.OUTPUT_NAME }}-APK path: app/build/outputs/apk/release/${{ env.OUTPUT_NAME }}.apk - name: Build release AAB @@ -59,8 +59,15 @@ jobs: run: mv "./app/build/outputs/bundle/release/app-release.aab" "./app/build/outputs/bundle/release/${{ env.OUTPUT_NAME }}.aab" - - name: Upload debug AAB - uses: actions/upload-artifact@v1 + - name: Upload release AAB + uses: actions/upload-artifact@v4 with: - name: ${{ env.OUTPUT_NAME }} - path: app/build/outputs/bundle/release/${{ env.OUTPUT_NAME }}.aab \ No newline at end of file + name: ${{ env.OUTPUT_NAME }}-AAB + path: app/build/outputs/bundle/release/${{ env.OUTPUT_NAME }}.aab + + - name: Merge Artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: merged-release-artifacts + pattern: ${{ env.OUTPUT_NAME }}-* + delete-merged: true diff --git a/.github/workflows/ci-android.yaml b/.github/workflows/ci-android.yaml index 51ffe41..0334a32 100644 --- a/.github/workflows/ci-android.yaml +++ b/.github/workflows/ci-android.yaml @@ -13,17 +13,17 @@ jobs: steps: - name: Checkout project sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: gradle - name: Build with Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 with: cache-read-only: ${{ github.ref != 'refs/heads/master' }} arguments: compileDebugSources --no-configuration-cache -Pnapt=true diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index e04a000..bf0d893 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -20,6 +20,11 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile +#Datastore +-keep class androidx.datastore.*.** {*;} +-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite* { + ; +} #https://stackoverflow.com/questions/42782328/retrofit-2-returns-null-in-release-apk-when-minifyenable-but-ok-in-debug-apk -keep class com.manuelnunez.apps.core.services.dto.* {*;} @@ -77,20 +82,39 @@ #Hilt -keep class androidx.hilt.* {*;} -## Fragment names --keepnames class * extends androidx.fragment.app.Fragment +#Serialization +# Keep `Companion` object fields of serializable classes. +# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. +-if @kotlinx.serialization.Serializable class ** +-keepclassmembers class <1> { + static <1>$Companion Companion; +} --keepnames class * extends android.os.Parcelable --keepnames class * extends java.io.Serializable +# Keep `serializer()` on companion objects (both default and named) of serializable classes. +-if @kotlinx.serialization.Serializable class ** { + static **$* *; +} +-keepclassmembers class <2>$<3> { + kotlinx.serialization.KSerializer serializer(...); +} + +# Keep `INSTANCE.serializer()` of serializable objects. +-if @kotlinx.serialization.Serializable class ** { + public static ** INSTANCE; +} +-keepclassmembers class <1> { + public static <1> INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} +# @Serializable and @Polymorphic are used at runtime for polymorphic serialization. +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault ### R8 ## Gson -keepclassmembers,allowobfuscation class * { @com.google.gson.annotations.SerializedName ; } --keep class com.google.gson.reflect.TypeToken { *; } --keep class * extends com.google.gson.reflect.TypeToken ## Kotlin suspend functions -keep class kotlin.coroutines.Continuation From 26e2fe606b39501f7098311b7465aca11ed8b686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com> Date: Fri, 19 Apr 2024 12:34:04 -0400 Subject: [PATCH 2/3] Add zoomable images --- .../core/ui/component/StatefulAsyncImage.kt | 87 +++++++++++++++++-- .../detail/ui/components/DetailScreen.kt | 12 ++- .../favorites/ui/component/FavoritesScreen.kt | 64 +++++++++++--- 3 files changed, 139 insertions(+), 24 deletions(-) diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/StatefulAsyncImage.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/StatefulAsyncImage.kt index 41b9e39..2b1645d 100644 --- a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/StatefulAsyncImage.kt +++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/StatefulAsyncImage.kt @@ -1,32 +1,45 @@ package com.manuelnunez.apps.core.ui.component import androidx.compose.foundation.Image +import androidx.compose.foundation.gestures.detectTransformGestures import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import coil.compose.AsyncImagePainter import coil.compose.AsyncImagePainter.State.Error import coil.compose.AsyncImagePainter.State.Loading import coil.compose.rememberAsyncImagePainter import com.manuelnunez.apps.core.ui.R +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.roundToInt +import kotlin.math.sin @Composable fun StatefulAsyncImage( modifier: Modifier = Modifier, imageUrl: String, contentDescription: String, + zoomable: Boolean = false, placeholder: Painter = painterResource(R.drawable.ic_broken_image), contentScale: ContentScale = ContentScale.Crop ) { @@ -53,11 +66,73 @@ fun StatefulAsyncImage( ) } - Image( - modifier = modifier, - painter = if (isError.not() && !isLocalInspection) imageLoader else placeholder, - contentScale = - if (isError.not() && !isLocalInspection) contentScale else ContentScale.FillBounds, - contentDescription = contentDescription) + if (zoomable) { + ZoomableImage( + modifier, + isError, + isLocalInspection, + imageLoader, + placeholder, + contentScale, + contentDescription) + } else { + Image( + modifier = modifier, + painter = if (isError.not() && !isLocalInspection) imageLoader else placeholder, + contentScale = + if (isError.not() && !isLocalInspection) contentScale else ContentScale.FillBounds, + contentDescription = contentDescription) + } } } + +@Composable +private fun ZoomableImage( + modifier: Modifier, + isError: Boolean, + isLocalInspection: Boolean, + imageLoader: AsyncImagePainter, + placeholder: Painter, + contentScale: ContentScale, + contentDescription: String +) { + val angle by remember { mutableFloatStateOf(0f) } + var zoom by remember { mutableFloatStateOf(1f) } + var offsetX by remember { mutableFloatStateOf(0f) } + var offsetY by remember { mutableFloatStateOf(0f) } + + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp.value + val screenHeight = configuration.screenHeightDp.dp.value + + Image( + modifier = + modifier + .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } + .graphicsLayer(scaleX = zoom, scaleY = zoom, rotationZ = angle) + .pointerInput(Unit) { + detectTransformGestures( + onGesture = { _, pan, gestureZoom, _ -> + zoom = (zoom * gestureZoom).coerceIn(1F..4F) + if (zoom > 1) { + val x = (pan.x * zoom) + val y = (pan.y * zoom) + val angleRad = angle * PI / 180.0 + + offsetX = + (offsetX + (x * cos(angleRad) - y * sin(angleRad)).toFloat()).coerceIn( + -(screenWidth * zoom)..(screenWidth * zoom)) + offsetY = + (offsetY + (x * sin(angleRad) + y * cos(angleRad)).toFloat()).coerceIn( + -(screenHeight * zoom)..(screenHeight * zoom)) + } else { + offsetX = 0F + offsetY = 0F + } + }) + }, + painter = if (isError.not() && !isLocalInspection) imageLoader else placeholder, + contentScale = + if (isError.not() && !isLocalInspection) contentScale else ContentScale.FillBounds, + contentDescription = contentDescription) +} diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt index 4c02095..8e12e30 100644 --- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt +++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeContent import androidx.compose.foundation.layout.windowInsetsPadding @@ -76,10 +77,11 @@ private fun DetailPortrait( modifier = Modifier.weight(1f).wrapContentSize(), horizontalAlignment = Alignment.CenterHorizontally) { StatefulAsyncImage( - modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp).heightIn(min = 180.dp), imageUrl = item.imageUrl, contentDescription = item.photoId, - contentScale = ContentScale.Fit) + contentScale = ContentScale.Fit, + zoomable = true) Row { ShareImage(url = item.imageUrl) @@ -128,10 +130,12 @@ private fun DetailLandscape( Row(verticalAlignment = Alignment.CenterVertically) { StatefulAsyncImage( - modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp).weight(0.7f), + modifier = + Modifier.fillMaxWidth().padding(vertical = 6.dp).weight(0.7f).heightIn(min = 180.dp), imageUrl = item.imageUrl, contentDescription = item.photoId, - contentScale = ContentScale.Fit) + contentScale = ContentScale.Fit, + zoomable = true) Column(Modifier.weight(0.1f)) { ShareImage(url = item.imageUrl) diff --git a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/component/FavoritesScreen.kt b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/component/FavoritesScreen.kt index 2ee9067..bd8f21c 100644 --- a/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/component/FavoritesScreen.kt +++ b/features/favorites/ui/src/main/kotlin/com/manuelnunez/apps/features/favorites/ui/component/FavoritesScreen.kt @@ -16,6 +16,8 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration @@ -24,6 +26,9 @@ import androidx.compose.ui.unit.dp import com.manuelnunez.apps.core.domain.model.Item import com.manuelnunez.apps.core.ui.component.ImageCard import com.manuelnunez.apps.core.ui.component.SurfaceText +import com.manuelnunez.apps.core.ui.theme.MainTheme +import com.manuelnunez.apps.core.ui.utils.OrientationPreviews +import com.manuelnunez.apps.core.ui.utils.ThemePreviews import com.manuelnunez.apps.features.favorites.ui.R @Composable @@ -50,19 +55,50 @@ private fun FavoriteItems(navigateToDetails: (Item) -> Unit, favoriteItems: List GridCells.Adaptive(minSize = 100.dp) } else GridCells.Fixed(3) - LazyVerticalGrid( - columns = cellConfiguration, - modifier = Modifier.fillMaxSize(), - state = gridState, - contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 10.dp, bottom = 20.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), - horizontalArrangement = Arrangement.spacedBy(10.dp)) { - items(items = favoriteItems) { item -> - ImageCard( - modifier = Modifier.size(width = 100.dp, height = 200.dp), - imageUrl = item.imageUrl, - cardContentDescription = item.description, - onClick = { navigateToDetails.invoke(item) }) + if (favoriteItems.isEmpty()) { + Text( + modifier = Modifier.fillMaxSize().padding(10.dp), + color = MaterialTheme.colorScheme.onSurface, + text = "Add your favorite cats, and they will be display here! ") + } else { + LazyVerticalGrid( + columns = cellConfiguration, + modifier = Modifier.fillMaxSize(), + state = gridState, + contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 10.dp, bottom = 20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp)) { + items(items = favoriteItems) { item -> + ImageCard( + modifier = Modifier.size(width = 100.dp, height = 200.dp), + imageUrl = item.imageUrl, + cardContentDescription = item.description, + onClick = { navigateToDetails.invoke(item) }) + } } - } + } +} + +@ThemePreviews +@OrientationPreviews +@Composable +fun FavoriteScreenEmptyPreview() { + MainTheme { FavoritesScreen(items = emptyList(), navigateToDetails = {}, onBackClick = {}) } +} + +@ThemePreviews +@OrientationPreviews +@Composable +fun FavoriteScreenPreview() { + MainTheme { + val items = + List(5) { index -> + Item( + "$index", + "https://example.com/$index", + description = "description: $index", + thumbnailUrl = "https://example.com/$index") + } + FavoritesScreen(items = items, navigateToDetails = {}, onBackClick = {}) + } } From ffed0e30107c313f59e134cc463de75dc21a9ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Nu=C3=B1ez?= <03.manu@gmail.com> Date: Fri, 19 Apr 2024 14:08:10 -0400 Subject: [PATCH 3/3] Updated github gradle action version --- .github/workflows/ci-android.yaml | 2 +- README.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-android.yaml b/.github/workflows/ci-android.yaml index 0334a32..d9de814 100644 --- a/.github/workflows/ci-android.yaml +++ b/.github/workflows/ci-android.yaml @@ -23,7 +23,7 @@ jobs: cache: gradle - name: Build with Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v3 with: cache-read-only: ${{ github.ref != 'refs/heads/master' }} arguments: compileDebugSources --no-configuration-cache -Pnapt=true diff --git a/README.md b/README.md index 3eb84e6..dc5492b 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,9 @@ heart. ## Screenshots -| ![Screenshot 2024-04-16 at 10 26 30 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/c08f645e-a362-4d2e-bfff-c99d8966e2b8) | ![Screenshot 2024-04-19 at 12 06 41 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/f7c9873c-8ab8-4215-b4f3-ed801dd8f6c9) | ![Screenshot 2024-04-19 at 12 17 14 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/534f2f34-5f9d-4945-83bf-0076f667bc19)| ![Screenshot 2024-04-19 at 12 10 40 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/69042702-71aa-4eae-b508-871d9c630ff8) | ![Screenshot 2024-04-19 at 12 16 02 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/e55d1bae-6aa4-4401-a6b6-8754b0756efd)| -|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| ![Screenshot 2024-04-16 at 10 26 30 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/c08f645e-a362-4d2e-bfff-c99d8966e2b8) | ![Screenshot 2024-04-19 at 12 06 41 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/f7c9873c-8ab8-4215-b4f3-ed801dd8f6c9) | ![Screenshot 2024-04-19 at 12 17 14 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/534f2f34-5f9d-4945-83bf-0076f667bc19) | ![Screenshot 2024-04-19 at 12 10 40 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/69042702-71aa-4eae-b508-871d9c630ff8) | ![Screenshot 2024-04-19 at 12 16 02 AM](https://github.com/manununhez/purrfect-pics/assets/5048531/e55d1bae-6aa4-4401-a6b6-8754b0756efd) | ![Screenshot 2024-04-19 at 2 15 49 PM](https://github.com/manununhez/purrfect-pics/assets/5048531/b4185b5a-85b7-47e0-91df-ab0e0d822c42) | +|:--:|:--:|:--:|:--:|:--:|:--:| +| Splashscreen | Home - Featured and popular items | Popular paginated items | Your favorites cats | Detail image (share and favorite button) | Detail image with zoomable elements| ## Considerations