diff --git a/README.md b/README.md index 475aa1e..d85bc37 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PurrfectPics +# PurrfectPics ![ic_launcher_round](https://github.com/manununhez/purrfect-pics/assets/5048531/1fab47b6-03fb-4901-b6c9-0fe60cbaecd1) PurrfectPics is your ultimate companion for discovering, customizing, and sharing adorable cat images on Android. With a wide range of features, PurrfectPics brings joy to every cat lover's @@ -16,23 +16,29 @@ heart. ### Android Libraries -- **UI Components:** [AndroidX Core KTX](https://developer.android.com/jetpack/androidx/releases/core) | [Material Components for Android](https://github.com/material-components/material-components-android) | [Compose UI](https://developer.android.com/jetpack/androidx/releases/compose-ui) -- **Testing:** [JUnit](https://junit.org/junit5/) | [MockK](https://mockk.io/) | [Turbine](https://github.com/cashapp/turbine) -- **Dependency Injection:** [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) +- **UI Components: + ** [AndroidX Core KTX](https://developer.android.com/jetpack/androidx/releases/core) | [Material Components for Android](https://github.com/material-components/material-components-android) | [Compose UI](https://developer.android.com/jetpack/androidx/releases/compose-ui) +- **Testing: + ** [JUnit](https://junit.org/junit5/) | [MockK](https://mockk.io/) | [Turbine](https://github.com/cashapp/turbine) +- **Dependency Injection: + ** [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) - **Coroutines:** [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) - **Networking:** [Retrofit](https://square.github.io/retrofit/) - **Image Loading:** [Coil](https://coil-kt.github.io/coil/) ### Compose Libraries -- **UI Components:** [Material3](https://developer.android.com/jetpack/androidx/releases/compose-material3) | [Compose UI](https://developer.android.com/jetpack/androidx/releases/compose-ui) -- **Navigation:** [Navigation Compose](https://developer.android.com/jetpack/androidx/releases/navigation) | [Hilt Navigation Compose](https://developer.android.com/training/dependency-injection/hilt-android#navigation-compose) -- **Material Design:** [Material Components for Android](https://github.com/material-components/material-components-android) +- **UI Components: + ** [Material3](https://developer.android.com/jetpack/androidx/releases/compose-material3) | [Compose UI](https://developer.android.com/jetpack/androidx/releases/compose-ui) +- **Navigation: + ** [Navigation Compose](https://developer.android.com/jetpack/androidx/releases/navigation) | [Hilt Navigation Compose](https://developer.android.com/training/dependency-injection/hilt-android#navigation-compose) +- **Material Design: + ** [Material Components for Android](https://github.com/material-components/material-components-android) ## Screenshots -| ![Screenshot 2024-04-15 at 6 34 16 PM](https://github.com/manununhez/purrfect-pics/assets/5048531/a8024c0c-e31f-4189-b268-1167048658ad) | ![Screenshot 2024-04-15 at 6 35 33 PM](https://github.com/manununhez/purrfect-pics/assets/5048531/1a72c5ea-ee22-4470-8e98-6215ebb86924) | ![Screenshot 2024-04-15 at 6 36 08 PM](https://github.com/manununhez/purrfect-pics/assets/5048531/c31637d7-ddfe-436a-8e16-d27244d638ea) | -|---|---|---| +| ![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-15 at 6 34 16 PM](https://github.com/manununhez/purrfect-pics/assets/5048531/a8024c0c-e31f-4189-b268-1167048658ad) | ![Screenshot 2024-04-15 at 6 35 33 PM](https://github.com/manununhez/purrfect-pics/assets/5048531/1a72c5ea-ee22-4470-8e98-6215ebb86924) | ![Screenshot 2024-04-15 at 6 36 08 PM](https://github.com/manununhez/purrfect-pics/assets/5048531/c31637d7-ddfe-436a-8e16-d27244d638ea) | +|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| ## Considerations @@ -44,7 +50,8 @@ heart. the other hand, Coil image lib does use disk and memory cache to smooth image loading. - KtfmtFormat plugin applied for code formatting. - There is a known issue with the splash screen not showing on Android 12. A temporary - solution is to **open the app from the app tray**, as indicated [here](https://stackoverflow.com/questions/69812590/android-12-splash-screen-icon-not-displaying)) + solution is to **open the app from the app tray**, as + indicated [here](https://stackoverflow.com/questions/69812590/android-12-splash-screen-icon-not-displaying)) ## Getting Started @@ -82,4 +89,4 @@ PurrfectPics is licensed under the [MIT License](LICENSE). ## About PurrfectPics is developed and maintained by [Manuel Nuñez](mailto:manuel.nunhez90@gmail.com). For -inquiries, please contact [manuel.nunhez90@gmail.com]. +inquiries, please contact [manuel.nunhez90@gmail.com]. \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a25818e..9b65ec8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,7 +69,7 @@ android { } dependencies { - implementation(projects.core.commonUi) + implementation(projects.core.ui) implementation(projects.core.data) implementation(projects.core.domain) implementation(projects.features.home.ui) diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt b/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt deleted file mode 100644 index 9b55bdd..0000000 --- a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AlertDialog.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.manuelnunez.apps.core.ui.component - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Info -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.vector.ImageVector -import com.manuelnunez.apps.core.ui.theme.MainTheme -import com.manuelnunez.apps.core.ui.utils.ThemePreviews - -@Composable -fun ErrorDialog(dialogTitle: String, dialogText: String, onConfirmation: () -> Unit) { - val openAlertDialog = remember { mutableStateOf(true) } - - when { - openAlertDialog.value -> { - ErrorAlertDialog( - onDismissRequest = { openAlertDialog.value = false }, - onConfirmation = { - openAlertDialog.value = false - onConfirmation.invoke() - }, - dialogTitle = dialogTitle, - dialogText = dialogText, - icon = Icons.Default.Info) - } - } -} - -@Composable -fun ErrorAlertDialog( - onDismissRequest: () -> Unit, - onConfirmation: () -> Unit, - dialogTitle: String, - dialogText: String, - icon: ImageVector, -) { - AlertDialog( - icon = { Icon(icon, contentDescription = "Example Icon") }, - title = { Text(text = dialogTitle) }, - text = { Text(text = dialogText) }, - onDismissRequest = { onDismissRequest() }, - confirmButton = { TextButton(onClick = { onConfirmation() }) { Text("Confirm") } }, - dismissButton = { TextButton(onClick = { onDismissRequest() }) { Text("Dismiss") } }) -} - -@ThemePreviews -@Composable -fun ErrorDialogPreview() { - MainTheme { - ErrorDialog( - onConfirmation = {}, - dialogTitle = "Title", - dialogText = "https://picsum.photos/id/237/200/300") - } -} diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt b/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt deleted file mode 100644 index 294e470..0000000 --- a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ImageCard.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.manuelnunez.apps.core.ui.component - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CardElevation -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import com.manuelnunez.apps.core.ui.theme.MainTheme -import com.manuelnunez.apps.core.ui.utils.ThemePreviews - -@Composable -fun ImageCard( - modifier: Modifier = Modifier, - imageUrl: String, - cardContentDescription: String, - elevation: CardElevation = CardDefaults.cardElevation(), - contentScale: ContentScale = ContentScale.Crop, - onClick: (() -> Unit)? = null, -) { - Card( - modifier = - modifier.clickable(onClick = { onClick?.invoke() }).semantics { - contentDescription = cardContentDescription - }, - elevation = elevation) { - DynamicAsyncImage( - modifier = Modifier.fillMaxSize(), - imageUrl = imageUrl, - contentDescription = "", - contentScale = contentScale) - } -} - -@ThemePreviews -@Composable -fun ImageCardPreview() { - MainTheme { - ImageCard( - onClick = {}, - cardContentDescription = "", - imageUrl = "https://picsum.photos/id/237/200/300") - } -} diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt b/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt deleted file mode 100644 index 24baa60..0000000 --- a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TextCard.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.manuelnunez.apps.core.ui.component - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextAlign -import com.manuelnunez.apps.core.ui.theme.MainTheme -import com.manuelnunez.apps.core.ui.utils.ThemePreviews - -@Composable -fun TextCard(modifier: Modifier = Modifier, text: String, onClick: () -> Unit) { - Card( - colors = - CardDefaults.cardColors().copy(containerColor = MaterialTheme.colorScheme.onBackground), - modifier = modifier.clickable(onClick = onClick).semantics { contentDescription = text }) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = text, - color = MaterialTheme.colorScheme.background) - } - } -} - -@ThemePreviews -@Composable -fun TextCardPreview() { - MainTheme { TextCard(onClick = {}, text = "See more") } -} diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt b/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt deleted file mode 100644 index a653a0e..0000000 --- a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/TitleText.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.manuelnunez.apps.core.ui.component - -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import com.manuelnunez.apps.core.ui.theme.MainTheme -import com.manuelnunez.apps.core.ui.utils.ThemePreviews - -@Composable -fun TitleText(modifier: Modifier = Modifier, title: String, textAlign: TextAlign? = null) { - Text( - modifier = modifier, - text = title, - textAlign = textAlign, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleLarge) -} - -@Composable -fun ErrorText(modifier: Modifier = Modifier, title: String, textAlign: TextAlign? = null) { - Text( - modifier = modifier, - text = title, - textAlign = textAlign, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleSmall) -} - -@ThemePreviews -@Composable -fun TitleTextPreview() { - MainTheme { TitleText(title = "See more") } -} - -@ThemePreviews -@Composable -fun ErrorTextPreview() { - MainTheme { ErrorText(title = "See more") } -} diff --git a/core/common/src/test/kotlin/com/manuelnunez/apps/core/common/EitherTest.kt b/core/common/src/test/kotlin/com/manuelnunez/apps/core/common/EitherTest.kt new file mode 100644 index 0000000..469aec0 --- /dev/null +++ b/core/common/src/test/kotlin/com/manuelnunez/apps/core/common/EitherTest.kt @@ -0,0 +1,27 @@ +package com.manuelnunez.apps.core.common + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class EitherTest { + + @Test + fun `test fold with success`() { + val successData = 42 + val successValue: Either = eitherSuccess(successData) + val result = successValue.fold(success = { "Success: $it" }, error = { "Error: $it" }) + + assertEquals(Either.Success(successData), successValue) + assertEquals("Success: $successData", result) + } + + @Test + fun `test fold with error`() { + val messageError = "An error occurred" + val errorValue: Either = eitherError(messageError) + val result = errorValue.fold(success = { "Success: $it" }, error = { "Error: $it" }) + + assertEquals(Either.Error(messageError), errorValue) + assertEquals(messageError, result) + } +} diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSource.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSource.kt index e4545fc..de9d737 100644 --- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSource.kt +++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSource.kt @@ -6,6 +6,7 @@ import androidx.paging.PagingData import com.manuelnunez.apps.core.common.Either import com.manuelnunez.apps.core.common.eitherError import com.manuelnunez.apps.core.common.eitherSuccess +import com.manuelnunez.apps.core.common.fold import com.manuelnunez.apps.core.data.PAGE_SIZE import com.manuelnunez.apps.core.data.PREFETCH_DISTANCE import com.manuelnunez.apps.core.data.datasource.paging.CataasCatsPagingSource @@ -15,7 +16,6 @@ import com.manuelnunez.apps.core.services.executors.RetrofitServiceRequest import com.manuelnunez.apps.core.services.executors.ServiceError import com.manuelnunez.apps.core.services.executors.ServicesExecutor import com.manuelnunez.apps.core.services.service.CataasService -import com.manuelnunez.apps.core.services.util.Result import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -33,8 +33,14 @@ constructor(private val servicesExecutor: ServicesExecutor, private val apiServi override fun getItems(): Either, ServiceError> { val response = servicesExecutor.execute(RetrofitServiceRequest(apiService.searchCats())) - return if (response is Result.Success) eitherSuccess(response.data.data) - else eitherError((response as Result.Error).exception) + return response.fold( + success = { + eitherSuccess(it.data) + }, + error = { + eitherError(it) + } + ) } override fun getAllItems(): Flow> = diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/PexeelsCatsRemoteDataSource.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/PexeelsCatsRemoteDataSource.kt index 0888244..2ca0ca6 100644 --- a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/PexeelsCatsRemoteDataSource.kt +++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/datasource/PexeelsCatsRemoteDataSource.kt @@ -6,6 +6,7 @@ import androidx.paging.PagingData import com.manuelnunez.apps.core.common.Either import com.manuelnunez.apps.core.common.eitherError import com.manuelnunez.apps.core.common.eitherSuccess +import com.manuelnunez.apps.core.common.fold import com.manuelnunez.apps.core.data.PAGE_SIZE import com.manuelnunez.apps.core.data.PREFETCH_DISTANCE import com.manuelnunez.apps.core.data.datasource.paging.PexeelsCatsPagingSource @@ -15,7 +16,6 @@ import com.manuelnunez.apps.core.services.executors.RetrofitServiceRequest import com.manuelnunez.apps.core.services.executors.ServiceError import com.manuelnunez.apps.core.services.executors.ServicesExecutor import com.manuelnunez.apps.core.services.service.PexelsService -import com.manuelnunez.apps.core.services.util.Result import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -33,8 +33,14 @@ constructor(private val servicesExecutor: ServicesExecutor, private val apiServi override fun getItems(): Either { val response = servicesExecutor.execute(RetrofitServiceRequest(apiService.searchCats())) - return if (response is Result.Success) eitherSuccess(response.data.data) - else eitherError((response as Result.Error).exception) + return response.fold( + success = { + eitherSuccess(it.data) + }, + error = { + eitherError(it) + } + ) } override fun getAllItems(): Flow> = diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt new file mode 100644 index 0000000..62f7130 --- /dev/null +++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt @@ -0,0 +1,96 @@ +package com.manuelnunez.apps.core.data.datasource + +import androidx.paging.PagingData +import androidx.paging.map +import app.cash.turbine.test +import com.manuelnunez.apps.core.common.Either +import com.manuelnunez.apps.core.common.eitherError +import com.manuelnunez.apps.core.common.eitherSuccess +import com.manuelnunez.apps.core.data.mapper.toItems +import com.manuelnunez.apps.core.data.utils.mockCataasResponseDTOS +import com.manuelnunez.apps.core.domain.model.Item +import com.manuelnunez.apps.core.services.dto.CataasResponseDTO +import com.manuelnunez.apps.core.services.executors.RetrofitServiceRequest +import com.manuelnunez.apps.core.services.executors.ServiceError +import com.manuelnunez.apps.core.services.executors.ServicesExecutor +import com.manuelnunez.apps.core.services.executors.toServiceResponse +import com.manuelnunez.apps.core.services.service.CataasService +import io.mockk.coEvery +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import retrofit2.Response + +class CataasCatsRemoteDataSourceTest { + private val servicesExecutor: ServicesExecutor = mockk() + private val apiService: CataasService = mockk() + private val mockServiceError = mockk() + + private lateinit var remoteDataSource: CataasCatsRemoteDataSource + + @BeforeEach + fun setup() { + remoteDataSource = CataasCatsRemoteDataSourceImpl(servicesExecutor, apiService) + } + + @Test + fun `when getItems is called successfully, then returns PexelsSearchResponseDTO`() { + val responseSuccess = Response.success(mockCataasResponseDTOS) + val resultSuccess = eitherSuccess(responseSuccess.toServiceResponse()) + + every { apiService.searchCats().execute() } returns responseSuccess + every { + servicesExecutor.execute(any>>()) + } returns resultSuccess + + val result = remoteDataSource.getItems() + + Assertions.assertTrue(result is Either.Success) + Assertions.assertEquals(eitherSuccess(mockCataasResponseDTOS), result) + verify(exactly = 1) { + apiService.searchCats() + servicesExecutor.execute(any>>()) + } + confirmVerified(apiService, servicesExecutor) + } + + @Test + fun `when getItems call fails, then returns ServiceError`() { + val responseSuccess = Response.success(mockCataasResponseDTOS) + val errorSuccess = eitherError(mockServiceError) + + every { apiService.searchCats().execute() } returns responseSuccess + every { + servicesExecutor.execute(any>>()) + } returns errorSuccess + + val result = remoteDataSource.getItems() + + Assertions.assertTrue(result is Either.Error) + Assertions.assertEquals(eitherError(mockServiceError), result) + verify(exactly = 1) { + apiService.searchCats() + servicesExecutor.execute(any>>()) + } + confirmVerified(apiService, servicesExecutor) + } + + @Test + fun `when getAllItems is called, then returns paginated Items`() = runTest { + val pagingData: PagingData = PagingData.from(mockCataasResponseDTOS.toItems()) + + coEvery { apiService.searchCatsPaginated(skip = any()) } returns mockCataasResponseDTOS + + val resultFlow = remoteDataSource.getAllItems() + + resultFlow.test { + pagingData.map { expected -> awaitItem().map { Assertions.assertEquals(expected, it) } } + cancelAndIgnoreRemainingEvents() + } + } +} diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt new file mode 100644 index 0000000..c56f297 --- /dev/null +++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt @@ -0,0 +1,97 @@ +package com.manuelnunez.apps.core.data.datasource + +import androidx.paging.PagingData +import androidx.paging.map +import app.cash.turbine.test +import com.manuelnunez.apps.core.common.Either +import com.manuelnunez.apps.core.common.eitherError +import com.manuelnunez.apps.core.common.eitherSuccess +import com.manuelnunez.apps.core.data.mapper.toItems +import com.manuelnunez.apps.core.data.utils.mockPexelsSearchResponseDTO +import com.manuelnunez.apps.core.domain.model.Item +import com.manuelnunez.apps.core.services.dto.PexelsSearchResponseDTO +import com.manuelnunez.apps.core.services.executors.RetrofitServiceRequest +import com.manuelnunez.apps.core.services.executors.ServiceError +import com.manuelnunez.apps.core.services.executors.ServicesExecutor +import com.manuelnunez.apps.core.services.executors.toServiceResponse +import com.manuelnunez.apps.core.services.service.PexelsService +import io.mockk.coEvery +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import retrofit2.Response + +class PexelsCatsRemoteDataSourceTest { + private val servicesExecutor: ServicesExecutor = mockk() + private val apiService: PexelsService = mockk() + private val mockServiceError = mockk() + + private lateinit var remoteDataSource: PexelsCatsRemoteDataSource + + @BeforeEach + fun setup() { + remoteDataSource = PexelsCatsRemoteDataSourceImpl(servicesExecutor, apiService) + } + + @Test + fun `when getItems is called successfully, then returns PexelsSearchResponseDTO`() { + val responseSuccess = Response.success(mockPexelsSearchResponseDTO) + val resultSuccess = eitherSuccess(responseSuccess.toServiceResponse()) + + every { apiService.searchCats().execute() } returns responseSuccess + every { + servicesExecutor.execute(any>()) + } returns resultSuccess + + val result = remoteDataSource.getItems() + + assertTrue(result is Either.Success) + assertEquals(eitherSuccess(mockPexelsSearchResponseDTO), result) + verify(exactly = 1) { + apiService.searchCats() + servicesExecutor.execute(any>()) + } + confirmVerified(apiService, servicesExecutor) + } + + @Test + fun `when getItems call fails, then returns ServiceError`() { + val responseSuccess = Response.success(mockPexelsSearchResponseDTO) + val errorSuccess = eitherError(mockServiceError) + + every { apiService.searchCats().execute() } returns responseSuccess + every { + servicesExecutor.execute(any>()) + } returns errorSuccess + + val result = remoteDataSource.getItems() + + assertTrue(result is Either.Error) + assertEquals(eitherError(mockServiceError), result) + verify(exactly = 1) { + apiService.searchCats() + servicesExecutor.execute(any>()) + } + confirmVerified(apiService, servicesExecutor) + } + + @Test + fun `when getAllItems is called, then returns paginated Items`() = runTest { + val pagingData: PagingData = PagingData.from(mockPexelsSearchResponseDTO.toItems()) + + coEvery { apiService.searchCatsPaginated(page = any()) } returns mockPexelsSearchResponseDTO + + val resultFlow = remoteDataSource.getAllItems() + + resultFlow.test { + pagingData.map { expected -> awaitItem().map { assertEquals(expected, it) } } + cancelAndIgnoreRemainingEvents() + } + } +} diff --git a/core/services/build.gradle.kts b/core/services/build.gradle.kts index eb7a8d8..8c50bdc 100644 --- a/core/services/build.gradle.kts +++ b/core/services/build.gradle.kts @@ -42,9 +42,13 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } buildFeatures { buildConfig = true } + + packaging { resources { excludes.add("META-INF/{LICENSE-notice.md,LICENSE.md}") } } } dependencies { + implementation(projects.core.common) + implementation(libs.hilt.android) ksp(libs.hilt.compiler) diff --git a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServiceExecutorRetrofitImpl.kt b/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServiceExecutorRetrofitImpl.kt index 6df6ea0..e96261c 100644 --- a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServiceExecutorRetrofitImpl.kt +++ b/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServiceExecutorRetrofitImpl.kt @@ -1,14 +1,16 @@ package com.manuelnunez.apps.core.services.executors import com.google.gson.JsonSyntaxException +import com.manuelnunez.apps.core.common.Either +import com.manuelnunez.apps.core.common.eitherError +import com.manuelnunez.apps.core.common.eitherSuccess import com.manuelnunez.apps.core.services.exception.NetworkException -import com.manuelnunez.apps.core.services.util.Result import retrofit2.Call import java.util.concurrent.TimeoutException class ServiceExecutorRetrofitImpl : ServicesExecutor { - override fun execute(request: ServiceRequest): Result> { + override fun execute(request: ServiceRequest): Either, ServiceError> { if (request !is RetrofitServiceRequest) { throw IllegalArgumentException("Accepted only RetrofitServiceRequest") } @@ -16,26 +18,26 @@ class ServiceExecutorRetrofitImpl : ServicesExecutor { return try { val response = request.retrofitCall.execute() if (response.isSuccessful) { - Result.Success( + eitherSuccess( ServiceResponse( - optData = response.body(), + body = response.body(), statusCode = response.code(), headers = response.headers().toMultimap())) } else { - Result.Error( + eitherError( ServiceError( description = response.errorBody()?.string(), statusCode = response.code(), headers = response.headers().toMultimap())) } } catch (ex: TimeoutException) { - Result.Error(ServiceError("Timeout error: ${ex.message}", -1, emptyMap())) + eitherError(ServiceError("Timeout error: ${ex.message}", -1, emptyMap())) } catch (ex: NetworkException) { - Result.Error(ServiceError("Network error: ${ex.message}", -1, emptyMap())) + eitherError(ServiceError("Network error: ${ex.message}", -1, emptyMap())) } catch (ex: JsonSyntaxException) { - Result.Error(ServiceError("Json data parsing error: ${ex.message}", -1, emptyMap())) + eitherError(ServiceError("Json data parsing error: ${ex.message}", -1, emptyMap())) } catch (e: Exception) { - Result.Error(ServiceError("Unknown error: ${e.message}", -1, emptyMap())) + eitherError(ServiceError("Unknown error: ${e.message}", -1, emptyMap())) } } } diff --git a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServicesExecutor.kt b/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServicesExecutor.kt index 641ed69..2b1108b 100644 --- a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServicesExecutor.kt +++ b/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/executors/ServicesExecutor.kt @@ -1,28 +1,32 @@ package com.manuelnunez.apps.core.services.executors +import com.manuelnunez.apps.core.common.Either import com.manuelnunez.apps.core.services.exception.NetworkException import com.manuelnunez.apps.core.services.exception.ServiceException -import com.manuelnunez.apps.core.services.util.Result +import retrofit2.Response interface ServicesExecutor { @Throws(NetworkException::class, ServiceException::class) - fun execute(request: ServiceRequest): Result> + fun execute(request: ServiceRequest): Either, ServiceError> } interface ServiceRequest -open class ServiceResponse( - val optData: T?, +class ServiceResponse( + val body: T?, val statusCode: Int, val headers: Map>, ) { val data: T - get() = optData!! + get() = body!! } -open class ServiceError( +class ServiceError( val description: String?, val statusCode: Int, val headers: Map>, ) + +fun Response.toServiceResponse() = + ServiceResponse(this.body(), this.code(), this.headers().toMultimap()) diff --git a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/util/Result.kt b/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/util/Result.kt deleted file mode 100644 index bfde2be..0000000 --- a/core/services/src/main/kotlin/com/manuelnunez/apps/core/services/util/Result.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.manuelnunez.apps.core.services.util - -import com.manuelnunez.apps.core.services.executors.ServiceError - -sealed interface Result { - data class Success(val data: T) : Result - - data class Error(val exception: ServiceError) : Result -} diff --git a/core/common-ui/.gitignore b/core/ui/.gitignore similarity index 100% rename from core/common-ui/.gitignore rename to core/ui/.gitignore diff --git a/core/common-ui/build.gradle.kts b/core/ui/build.gradle.kts similarity index 77% rename from core/common-ui/build.gradle.kts rename to core/ui/build.gradle.kts index c2d373d..79a3351 100644 --- a/core/common-ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -12,7 +12,10 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - defaultConfig { minSdk = 21 } + defaultConfig { + minSdk = 21 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } @@ -33,4 +36,8 @@ dependencies { val composeBom = platform(libs.androidx.compose.bom) implementation(composeBom) debugImplementation(libs.androidx.ui.tooling) + + testImplementation(libs.junit) + api(libs.androidx.compose.ui.test.junit4) + debugApi(libs.androidx.compose.ui.test.manifest) } diff --git a/core/ui/src/androidTest/kotlin/com/manuelnunez/apps/core/ui/ThemeTest.kt b/core/ui/src/androidTest/kotlin/com/manuelnunez/apps/core/ui/ThemeTest.kt new file mode 100644 index 0000000..f6e7303 --- /dev/null +++ b/core/ui/src/androidTest/kotlin/com/manuelnunez/apps/core/ui/ThemeTest.kt @@ -0,0 +1,167 @@ +package com.manuelnunez.apps.core.ui + +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.unit.dp +import com.manuelnunez.apps.core.ui.theme.BackgroundTheme +import com.manuelnunez.apps.core.ui.theme.DarkColorScheme +import com.manuelnunez.apps.core.ui.theme.GradientColors +import com.manuelnunez.apps.core.ui.theme.LightColorScheme +import com.manuelnunez.apps.core.ui.theme.LocalBackgroundTheme +import com.manuelnunez.apps.core.ui.theme.LocalGradientColors +import com.manuelnunez.apps.core.ui.theme.MainTheme +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +class ThemeTest { + + @get:Rule val composeTestRule = createComposeRule() + + @Test + fun darkThemeFalse_dynamicColorFalse_androidThemeFalse() { + composeTestRule.setContent { + MainTheme( + darkTheme = false, + disableDynamicTheming = true, + ) { + val colorScheme = LightColorScheme + assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) + val gradientColors = defaultGradientColors(colorScheme) + assertEquals(gradientColors, LocalGradientColors.current) + val backgroundTheme = defaultBackgroundTheme(colorScheme) + assertEquals(backgroundTheme, LocalBackgroundTheme.current) + } + } + } + + @Test + fun darkThemeTrue_dynamicColorFalse_androidThemeFalse() { + composeTestRule.setContent { + MainTheme( + darkTheme = true, + disableDynamicTheming = true, + ) { + val colorScheme = DarkColorScheme + assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) + val gradientColors = defaultGradientColors(colorScheme) + assertEquals(gradientColors, LocalGradientColors.current) + val backgroundTheme = defaultBackgroundTheme(colorScheme) + assertEquals(backgroundTheme, LocalBackgroundTheme.current) + } + } + } + + @Test + fun darkThemeFalse_dynamicColorTrue_androidThemeFalse() { + composeTestRule.setContent { + MainTheme( + darkTheme = false, + disableDynamicTheming = false, + ) { + val colorScheme = dynamicLightColorSchemeWithFallback() + assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) + val gradientColors = dynamicGradientColorsWithFallback(colorScheme) + assertEquals(gradientColors, LocalGradientColors.current) + val backgroundTheme = defaultBackgroundTheme(colorScheme) + assertEquals(backgroundTheme, LocalBackgroundTheme.current) + } + } + } + + @Test + fun darkThemeTrue_dynamicColorTrue_androidThemeFalse() { + composeTestRule.setContent { + MainTheme( + darkTheme = true, + disableDynamicTheming = false, + ) { + val colorScheme = dynamicDarkColorSchemeWithFallback() + assertColorSchemesEqual(colorScheme, MaterialTheme.colorScheme) + val gradientColors = dynamicGradientColorsWithFallback(colorScheme) + assertEquals(gradientColors, LocalGradientColors.current) + val backgroundTheme = defaultBackgroundTheme(colorScheme) + assertEquals(backgroundTheme, LocalBackgroundTheme.current) + } + } + } + + @Composable + private fun dynamicLightColorSchemeWithFallback(): ColorScheme = + when { + SDK_INT >= VERSION_CODES.S -> dynamicLightColorScheme(LocalContext.current) + else -> LightColorScheme + } + + @Composable + private fun dynamicDarkColorSchemeWithFallback(): ColorScheme = + when { + SDK_INT >= VERSION_CODES.S -> dynamicDarkColorScheme(LocalContext.current) + else -> DarkColorScheme + } + + private fun emptyGradientColors(colorScheme: ColorScheme): GradientColors = + GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp)) + + private fun defaultGradientColors(colorScheme: ColorScheme): GradientColors = + GradientColors( + top = colorScheme.inverseOnSurface, + bottom = colorScheme.primaryContainer, + container = colorScheme.surface, + ) + + private fun dynamicGradientColorsWithFallback(colorScheme: ColorScheme): GradientColors = + when { + SDK_INT >= VERSION_CODES.S -> emptyGradientColors(colorScheme) + else -> defaultGradientColors(colorScheme) + } + + private fun defaultBackgroundTheme(colorScheme: ColorScheme): BackgroundTheme = + BackgroundTheme( + color = colorScheme.surface, + tonalElevation = 2.dp, + ) + + /** Workaround for the fact that the NiA design system specify all color scheme values. */ + private fun assertColorSchemesEqual( + expectedColorScheme: ColorScheme, + actualColorScheme: ColorScheme, + ) { + assertEquals(expectedColorScheme.primary, actualColorScheme.primary) + assertEquals(expectedColorScheme.onPrimary, actualColorScheme.onPrimary) + assertEquals(expectedColorScheme.primaryContainer, actualColorScheme.primaryContainer) + assertEquals(expectedColorScheme.onPrimaryContainer, actualColorScheme.onPrimaryContainer) + assertEquals(expectedColorScheme.secondary, actualColorScheme.secondary) + assertEquals(expectedColorScheme.onSecondary, actualColorScheme.onSecondary) + assertEquals(expectedColorScheme.secondaryContainer, actualColorScheme.secondaryContainer) + assertEquals( + expectedColorScheme.onSecondaryContainer, + actualColorScheme.onSecondaryContainer, + ) + assertEquals(expectedColorScheme.tertiary, actualColorScheme.tertiary) + assertEquals(expectedColorScheme.onTertiary, actualColorScheme.onTertiary) + assertEquals(expectedColorScheme.tertiaryContainer, actualColorScheme.tertiaryContainer) + assertEquals(expectedColorScheme.onTertiaryContainer, actualColorScheme.onTertiaryContainer) + assertEquals(expectedColorScheme.error, actualColorScheme.error) + assertEquals(expectedColorScheme.onError, actualColorScheme.onError) + assertEquals(expectedColorScheme.errorContainer, actualColorScheme.errorContainer) + assertEquals(expectedColorScheme.onErrorContainer, actualColorScheme.onErrorContainer) + assertEquals(expectedColorScheme.background, actualColorScheme.background) + assertEquals(expectedColorScheme.onBackground, actualColorScheme.onBackground) + assertEquals(expectedColorScheme.surface, actualColorScheme.surface) + assertEquals(expectedColorScheme.onSurface, actualColorScheme.onSurface) + assertEquals(expectedColorScheme.surfaceVariant, actualColorScheme.surfaceVariant) + assertEquals(expectedColorScheme.onSurfaceVariant, actualColorScheme.onSurfaceVariant) + assertEquals(expectedColorScheme.inverseSurface, actualColorScheme.inverseSurface) + assertEquals(expectedColorScheme.inverseOnSurface, actualColorScheme.inverseOnSurface) + assertEquals(expectedColorScheme.outline, actualColorScheme.outline) + } +} diff --git a/core/common-ui/src/main/AndroidManifest.xml b/core/ui/src/main/AndroidManifest.xml similarity index 100% rename from core/common-ui/src/main/AndroidManifest.xml rename to core/ui/src/main/AndroidManifest.xml diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AdaptableVerticalGrid.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AdaptableVerticalGrid.kt similarity index 100% rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AdaptableVerticalGrid.kt rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/AdaptableVerticalGrid.kt diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Background.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Background.kt similarity index 100% rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Background.kt rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/Background.kt diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/CustomCard.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/CustomCard.kt new file mode 100644 index 0000000..9d0c7db --- /dev/null +++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/CustomCard.kt @@ -0,0 +1,95 @@ +package com.manuelnunez.apps.core.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CardElevation +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import com.manuelnunez.apps.core.ui.component.CustomCardAutomation.IMAGE_CARD_PREFIX +import com.manuelnunez.apps.core.ui.component.CustomCardAutomation.TEXT_CARD_PREFIX +import com.manuelnunez.apps.core.ui.theme.MainTheme +import com.manuelnunez.apps.core.ui.utils.ThemePreviews + +object CustomCardAutomation { + const val IMAGE_CARD_PREFIX = "ImageCard" + const val TEXT_CARD_PREFIX = "TextCard" +} + +@Composable +fun ImageCard( + modifier: Modifier = Modifier, + imageUrl: String, + cardContentDescription: String, + elevation: CardElevation = CardDefaults.cardElevation(), + contentScale: ContentScale = ContentScale.Crop, + testTag: String = IMAGE_CARD_PREFIX, + onClick: (() -> Unit)? = null +) { + Card( + modifier = + modifier + .clickable(onClick = { onClick?.invoke() }) + .semantics { contentDescription = cardContentDescription } + .testTag(testTag), + elevation = elevation) { + StatefulAsyncImage( + modifier = Modifier.fillMaxSize(), + imageUrl = imageUrl, + contentDescription = "", + contentScale = contentScale) + } +} + +@Composable +fun TextCard( + modifier: Modifier = Modifier, + text: String, + testTag: String = TEXT_CARD_PREFIX, + onClick: (() -> Unit)? = null +) { + Card( + colors = + CardDefaults.cardColors().copy(containerColor = MaterialTheme.colorScheme.onBackground), + modifier = + modifier + .clickable(onClick = { onClick?.invoke() }) + .semantics { contentDescription = text } + .testTag(testTag)) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = text, + color = MaterialTheme.colorScheme.background) + } + } +} + +@ThemePreviews +@Composable +fun ImageCardPreview() { + MainTheme { + ImageCard( + onClick = {}, + cardContentDescription = "", + imageUrl = "https://picsum.photos/id/237/200/300") + } +} + +@ThemePreviews +@Composable +fun TextCardPreview() { + MainTheme { TextCard(onClick = {}, text = "See more") } +} diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ErrorDialog.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ErrorDialog.kt new file mode 100644 index 0000000..e12188f --- /dev/null +++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/ErrorDialog.kt @@ -0,0 +1,73 @@ +package com.manuelnunez.apps.core.ui.component + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import com.manuelnunez.apps.core.ui.R +import com.manuelnunez.apps.core.ui.theme.MainTheme +import com.manuelnunez.apps.core.ui.utils.ThemePreviews + +@Composable +fun ErrorDialog( + dialogTitle: String, + dialogText: String, + onConfirmClick: () -> Unit, + icon: ImageVector = Icons.Default.Warning, + confirmButtonText: String = stringResource(id = R.string.alert_dialog_confirm_button), + dismissButtonText: String = stringResource(id = R.string.alert_dialog_dismiss_button) +) { + val openAlertDialog = remember { mutableStateOf(true) } + + val onDismissRequest = { openAlertDialog.value = false } + + val onConfirmation = { + openAlertDialog.value = false + onConfirmClick.invoke() + } + + when { + openAlertDialog.value -> { + AlertDialog( + icon = { Icon(icon, contentDescription = "Warning Icon") }, + title = { Text(text = dialogTitle) }, + text = { Text(text = dialogText) }, + onDismissRequest = { onDismissRequest() }, + confirmButton = { + TextButton( + modifier = Modifier.semantics { contentDescription = confirmButtonText }, + onClick = { onConfirmation() }) { + Text(confirmButtonText) + } + }, + dismissButton = { + TextButton( + modifier = Modifier.semantics { contentDescription = dismissButtonText }, + onClick = { onDismissRequest() }) { + Text(dismissButtonText) + } + }) + } + } +} + +@ThemePreviews +@Composable +fun ErrorDialogPreview() { + MainTheme { + ErrorDialog( + onConfirmClick = {}, + dialogTitle = "Title", + dialogText = "https://picsum.photos/id/237/200/300") + } +} diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/DynamicAsyncImage.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/StatefulAsyncImage.kt similarity index 93% rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/DynamicAsyncImage.kt rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/StatefulAsyncImage.kt index b68071b..41b9e39 100644 --- a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/DynamicAsyncImage.kt +++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/StatefulAsyncImage.kt @@ -17,15 +17,13 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage import coil.compose.AsyncImagePainter.State.Error import coil.compose.AsyncImagePainter.State.Loading import coil.compose.rememberAsyncImagePainter import com.manuelnunez.apps.core.ui.R -/** A wrapper around [AsyncImage] which determines the colorFilter based on the theme */ @Composable -fun DynamicAsyncImage( +fun StatefulAsyncImage( modifier: Modifier = Modifier, imageUrl: String, contentDescription: String, diff --git a/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/SurfaceText.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/SurfaceText.kt new file mode 100644 index 0000000..c2eafeb --- /dev/null +++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/component/SurfaceText.kt @@ -0,0 +1,33 @@ +package com.manuelnunez.apps.core.ui.component + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import com.manuelnunez.apps.core.ui.theme.MainTheme +import com.manuelnunez.apps.core.ui.utils.ThemePreviews + +@Composable +fun SurfaceText( + modifier: Modifier = Modifier, + text: String, + textAlign: TextAlign? = null, + style: TextStyle = MaterialTheme.typography.titleLarge +) { + Text( + modifier = modifier.semantics { contentDescription = text }, + text = text, + textAlign = textAlign, + color = MaterialTheme.colorScheme.onSurface, + style = style) +} + +@ThemePreviews +@Composable +fun TitleTextPreview() { + MainTheme { SurfaceText(text = "See more") } +} diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Background.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Background.kt similarity index 100% rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Background.kt rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Background.kt diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Color.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Color.kt similarity index 100% rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Color.kt rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Color.kt diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Gradient.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Gradient.kt similarity index 100% rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Gradient.kt rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Gradient.kt diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt similarity index 98% rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt index fdaa029..c35280a 100644 --- a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt +++ b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Theme.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.manuelnunez.apps.core.ui.component.MainGradientBackground -private val LightColorScheme = +val LightColorScheme = lightColorScheme( primary = primaryLight, onPrimary = onPrimaryLight, @@ -54,7 +54,7 @@ private val LightColorScheme = surfaceContainerHighest = surfaceContainerHighestLight, ) -private val DarkColorScheme = +val DarkColorScheme = darkColorScheme( primary = primaryDark, onPrimary = onPrimaryDark, diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Type.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Type.kt similarity index 100% rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Type.kt rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/theme/Type.kt diff --git a/core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/utils/Preview.kt b/core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/utils/Preview.kt similarity index 100% rename from core/common-ui/src/main/kotlin/com/manuelnunez/apps/core/ui/utils/Preview.kt rename to core/ui/src/main/kotlin/com/manuelnunez/apps/core/ui/utils/Preview.kt diff --git a/core/common-ui/src/main/res/drawable/ic_broken_image.xml b/core/ui/src/main/res/drawable/ic_broken_image.xml similarity index 100% rename from core/common-ui/src/main/res/drawable/ic_broken_image.xml rename to core/ui/src/main/res/drawable/ic_broken_image.xml diff --git a/core/common-ui/src/main/res/font/nunito_sans.ttf b/core/ui/src/main/res/font/nunito_sans.ttf similarity index 100% rename from core/common-ui/src/main/res/font/nunito_sans.ttf rename to core/ui/src/main/res/font/nunito_sans.ttf diff --git a/core/common-ui/src/main/res/font/nunito_sans_bold.ttf b/core/ui/src/main/res/font/nunito_sans_bold.ttf similarity index 100% rename from core/common-ui/src/main/res/font/nunito_sans_bold.ttf rename to core/ui/src/main/res/font/nunito_sans_bold.ttf diff --git a/core/common-ui/src/main/res/font/nunito_sans_light.ttf b/core/ui/src/main/res/font/nunito_sans_light.ttf similarity index 100% rename from core/common-ui/src/main/res/font/nunito_sans_light.ttf rename to core/ui/src/main/res/font/nunito_sans_light.ttf diff --git a/core/common-ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml similarity index 85% rename from core/common-ui/src/main/res/values/strings.xml rename to core/ui/src/main/res/values/strings.xml index 427cdee..d4bf228 100644 --- a/core/common-ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -10,4 +10,7 @@ "Ups!" Retry An Error has occurred. Please go back and try again. + Confirm + Dismiss + \ No newline at end of file diff --git a/features/detail/ui/build.gradle.kts b/features/detail/ui/build.gradle.kts index daa113e..b3c9141 100644 --- a/features/detail/ui/build.gradle.kts +++ b/features/detail/ui/build.gradle.kts @@ -34,7 +34,7 @@ android { dependencies { implementation(projects.core.common) implementation(projects.core.domain) - implementation(projects.core.commonUi) + implementation(projects.core.ui) // Arch Components implementation(libs.androidx.lifecycle.runtime.compose) diff --git a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailViewTest.kt b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailViewTest.kt index dcfdccd..0f5f1e7 100644 --- a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailViewTest.kt +++ b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/feature/detail/ui/DetailViewTest.kt @@ -2,6 +2,7 @@ package com.manuelnunez.apps.feature.detail.ui import androidx.activity.ComponentActivity import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertHasNoClickAction import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText @@ -22,12 +23,7 @@ class DetailViewTest { composeTestRule.setContent { DetailScreen(mockItem, onBackClick = {}) } // description - composeTestRule - .onNodeWithText( - mockItem.description, - substring = true, - ) - .assertExists() + composeTestRule.onNodeWithText(mockItem.description).assertExists() // share button composeTestRule @@ -40,12 +36,9 @@ class DetailViewTest { // Image composeTestRule - .onNodeWithContentDescription( - mockItem.photoId, - substring = true, - ) + .onNodeWithContentDescription(mockItem.photoId) .assertExists() - .assertHasClickAction() + .assertHasNoClickAction() } @Test @@ -61,7 +54,7 @@ class DetailViewTest { composeTestRule .onNodeWithContentDescription( - composeTestRule.activity.resources.getString(RCU.string.button_back), + composeTestRule.activity.resources.getString(RCU.string.alert_dialog_confirm_button), substring = true, ) .assertExists() diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailErrorScreen.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailErrorScreen.kt index fbaa683..a0153e5 100644 --- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailErrorScreen.kt +++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailErrorScreen.kt @@ -10,7 +10,7 @@ import com.manuelnunez.apps.core.ui.R as RCU @Composable fun DetailErrorScreen(onBackClick: () -> Unit) { ErrorDialog( - onConfirmation = onBackClick, + onConfirmClick = onBackClick, dialogTitle = stringResource(id = RCU.string.alert_error_title), dialogText = stringResource(id = RCU.string.alert_error_try_again_back)) } diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailScreen.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailScreen.kt index cf35c5f..f200c72 100644 --- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailScreen.kt +++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/feature/detail/ui/components/DetailScreen.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import com.manuelnunez.apps.core.domain.model.Item -import com.manuelnunez.apps.core.ui.component.DynamicAsyncImage -import com.manuelnunez.apps.core.ui.component.TitleText +import com.manuelnunez.apps.core.ui.component.StatefulAsyncImage +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.features.detail.ui.R @@ -44,6 +44,7 @@ import com.manuelnunez.apps.core.ui.R as RCU @Composable fun DetailScreen(item: Item, onBackClick: () -> Unit) { val orientation = LocalConfiguration.current.orientation + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { DetailLandscape(item, onBackClick) } else { @@ -61,9 +62,9 @@ private fun DetailPortrait(item: Item, onBackClick: () -> Unit) { Spacer(modifier = Modifier.height(10.dp)) Column( - modifier = Modifier.weight(1f).wrapContentSize().heightIn(100.dp), + modifier = Modifier.weight(1f).wrapContentSize().heightIn(min = 100.dp), horizontalAlignment = Alignment.CenterHorizontally) { - DynamicAsyncImage( + StatefulAsyncImage( modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp), imageUrl = item.imageUrl, contentDescription = item.photoId, @@ -71,12 +72,11 @@ private fun DetailPortrait(item: Item, onBackClick: () -> Unit) { ShareImage(url = item.imageUrl) - Text( + SurfaceText( modifier = Modifier.padding(top = 10.dp).padding(horizontal = 40.dp), textAlign = TextAlign.Center, text = item.description, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurface) + style = MaterialTheme.typography.titleSmall) } Spacer(modifier = Modifier.height(20.dp)) @@ -93,7 +93,7 @@ private fun DetailToolbar(onBackClick: () -> Unit) { tint = MaterialTheme.colorScheme.onSurface) } - TitleText(title = "Details") + SurfaceText(text = "Details") } } @@ -107,7 +107,7 @@ private fun DetailLandscape(item: Item, onBackClick: () -> Unit) { Spacer(modifier = Modifier.height(10.dp)) Row(verticalAlignment = Alignment.CenterVertically) { - DynamicAsyncImage( + StatefulAsyncImage( modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp).weight(0.7f), imageUrl = item.imageUrl, contentDescription = item.photoId, diff --git a/features/home/ui/build.gradle.kts b/features/home/ui/build.gradle.kts index 14eb1ab..b7a8b4d 100644 --- a/features/home/ui/build.gradle.kts +++ b/features/home/ui/build.gradle.kts @@ -34,7 +34,7 @@ android { dependencies { implementation(projects.core.common) implementation(projects.core.domain) - implementation(projects.core.commonUi) + implementation(projects.core.ui) implementation(projects.features.home.domain) // Arch Components diff --git a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt index 6c9e24c..e1cf49d 100644 --- a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt +++ b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeViewTest.kt @@ -5,10 +5,9 @@ import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText +import com.manuelnunez.apps.core.domain.model.Item import com.manuelnunez.apps.features.home.ui.components.HomeErrorScreen import com.manuelnunez.apps.features.home.ui.components.HomeScreen -import com.manuelnunez.apps.features.home.ui.utils.mockFeaturedPhotos -import com.manuelnunez.apps.features.home.ui.utils.mockPopularPhotos import org.junit.Rule import org.junit.Test import com.manuelnunez.apps.core.ui.R as RCU @@ -143,4 +142,28 @@ class HomeViewTest { ) .assertExists() } + + private val mockPopularPhotos = + List(20) { index -> + val id = (index + 1).toString() + Item( + photoId = id, + imageUrl = "https://example.com/photo$id", + thumbnailUrl = "https://example.com/photo$id/small", + description = "This is a description for popular items $id") + } + .shuffled() + .take(10) + + private val mockFeaturedPhotos = + List(20) { index -> + val id = (index + 1).toString() + Item( + photoId = id, + imageUrl = "https://example.com/photo$id", + thumbnailUrl = "https://example.com/photo$id/small", + description = "This is a description for featured items $id") + } + .shuffled() + .take(5) } diff --git a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/utils/MockUtils.kt b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/utils/MockUtils.kt deleted file mode 100644 index 97d9d69..0000000 --- a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/utils/MockUtils.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.manuelnunez.apps.features.home.ui.utils - -import com.manuelnunez.apps.core.domain.model.Item - -val mockPopularPhotos = - List(20) { index -> - val id = (index + 1).toString() - Item( - photoId = id, - imageUrl = "https://example.com/photo$id", - thumbnailUrl = "https://example.com/photo$id/small", - description = "This is a description for popular items $id") - } - .shuffled() - .take(10) - -val mockFeaturedPhotos = - List(20) { index -> - val id = (index + 1).toString() - Item( - photoId = id, - imageUrl = "https://example.com/photo$id", - thumbnailUrl = "https://example.com/photo$id/small", - description = "This is a description for featured items $id") - } - .shuffled() - .take(5) diff --git a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeErrorScreen.kt b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeErrorScreen.kt index d094fea..7004a84 100644 --- a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeErrorScreen.kt +++ b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeErrorScreen.kt @@ -2,41 +2,34 @@ package com.manuelnunez.apps.features.home.ui.components import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.manuelnunez.apps.core.ui.component.ErrorDialog -import com.manuelnunez.apps.core.ui.component.ErrorText +import com.manuelnunez.apps.core.ui.component.SurfaceText import com.manuelnunez.apps.core.ui.theme.MainTheme import com.manuelnunez.apps.core.ui.utils.FontScalingPreviews import com.manuelnunez.apps.core.ui.utils.ThemePreviews -import com.manuelnunez.apps.features.home.ui.R import com.manuelnunez.apps.core.ui.R as RCU @Composable fun HomeErrorScreen(retry: () -> Unit) { ErrorDialog( - onConfirmation = retry, + onConfirmClick = retry, dialogTitle = stringResource(id = RCU.string.alert_error_title), dialogText = stringResource(id = RCU.string.alert_error_try_again)) } @Composable -fun FeatureError() { - ErrorText( +fun ItemError(textError: String) { + SurfaceText( modifier = Modifier.padding(vertical = 6.dp, horizontal = 20.dp).fillMaxWidth(), textAlign = TextAlign.Center, - title = stringResource(id = R.string.alert_error_feature)) -} - -@Composable -fun PopularError() { - ErrorText( - modifier = Modifier.padding(vertical = 6.dp, horizontal = 20.dp).fillMaxWidth(), - textAlign = TextAlign.Center, - title = stringResource(id = R.string.alert_error_popular)) + text = textError, + style = MaterialTheme.typography.titleSmall) } @FontScalingPreviews diff --git a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt index 33f75c5..0284e53 100644 --- a/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt +++ b/features/home/ui/src/main/kotlin/com/manuelnunez/apps/features/home/ui/components/HomeScreen.kt @@ -28,8 +28,8 @@ import com.manuelnunez.apps.core.domain.model.Item import com.manuelnunez.apps.core.ui.component.AdaptableVerticalGrid import com.manuelnunez.apps.core.ui.component.AdaptableVerticalGridDecoration import com.manuelnunez.apps.core.ui.component.ImageCard +import com.manuelnunez.apps.core.ui.component.SurfaceText import com.manuelnunez.apps.core.ui.component.TextCard -import com.manuelnunez.apps.core.ui.component.TitleText import com.manuelnunez.apps.core.ui.theme.MainTheme import com.manuelnunez.apps.core.ui.utils.FontScalingPreviews import com.manuelnunez.apps.core.ui.utils.ThemePreviews @@ -56,7 +56,8 @@ fun HomeScreen( LoadingIndicator( loaderContentDescription = stringResource(id = RCU.string.section_feature)) } - FeaturedItemsState.Error -> item { FeatureError() } + FeaturedItemsState.Error -> + item { ItemError(stringResource(id = R.string.alert_error_feature)) } else -> {} } @@ -70,7 +71,8 @@ fun HomeScreen( LoadingIndicator( loaderContentDescription = stringResource(id = RCU.string.section_popular)) } - PopularItemsState.Error -> item { PopularError() } + PopularItemsState.Error -> + item { ItemError(stringResource(id = R.string.alert_error_popular)) } else -> {} } } @@ -79,9 +81,9 @@ fun HomeScreen( @Composable private fun FeaturedItem(items: List, navigateToDetails: (Item) -> Unit) { Column { - TitleText( + SurfaceText( modifier = Modifier.padding(vertical = 6.dp, horizontal = 20.dp), - title = stringResource(id = RCU.string.section_feature)) + text = stringResource(id = RCU.string.section_feature)) Spacer(modifier = Modifier.height(10.dp)) @@ -115,9 +117,9 @@ private fun PopularItem( val gridPadding = 20.dp - horizontalMarginItem Column { - TitleText( + SurfaceText( modifier = Modifier.padding(vertical = 6.dp, horizontal = 20.dp), - title = stringResource(id = RCU.string.section_popular)) + text = stringResource(id = RCU.string.section_popular)) Spacer(modifier = Modifier.height(10.dp)) diff --git a/features/seemore/ui/build.gradle.kts b/features/seemore/ui/build.gradle.kts index 39e3ff9..ce805dc 100644 --- a/features/seemore/ui/build.gradle.kts +++ b/features/seemore/ui/build.gradle.kts @@ -35,7 +35,7 @@ android { dependencies { implementation(projects.core.common) implementation(projects.core.domain) - implementation(projects.core.commonUi) + implementation(projects.core.ui) implementation(projects.features.seemore.domain) // Arch Components diff --git a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreErrorScreen.kt b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreErrorScreen.kt index 55fab69..ea9f228 100644 --- a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreErrorScreen.kt +++ b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreErrorScreen.kt @@ -11,7 +11,7 @@ import com.manuelnunez.apps.core.ui.R as RCU @Composable fun SeeMoreErrorScreen(retry: () -> Unit) { ErrorDialog( - onConfirmation = retry, + onConfirmClick = retry, dialogTitle = stringResource(id = RCU.string.alert_error_title), dialogText = stringResource(id = RCU.string.alert_error_try_again)) } diff --git a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreScreen.kt b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreScreen.kt index 9d9a582..bc18564 100644 --- a/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreScreen.kt +++ b/features/seemore/ui/src/main/kotlin/com/manuelnunez/apps/feature/seemore/ui/components/SeeMoreScreen.kt @@ -36,7 +36,7 @@ import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemKey import com.manuelnunez.apps.core.domain.model.Item import com.manuelnunez.apps.core.ui.component.ImageCard -import com.manuelnunez.apps.core.ui.component.TitleText +import com.manuelnunez.apps.core.ui.component.SurfaceText import com.manuelnunez.apps.core.ui.theme.MainTheme import com.manuelnunez.apps.core.ui.utils.FontScalingPreviews import com.manuelnunez.apps.core.ui.utils.ThemePreviews @@ -108,7 +108,7 @@ private fun SeeMoreToolbar(onBackClick: () -> Unit) { tint = MaterialTheme.colorScheme.onSurface) } - TitleText(title = stringResource(id = RCU.string.section_popular)) + SurfaceText(text = stringResource(id = RCU.string.section_popular)) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5547f45..96ffefe 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,7 +34,7 @@ include(":core:domain") include(":core:services") -include(":core:common-ui") +include(":core:ui") include(":features:home:domain")