diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..81cf63d --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +RoomJetpackCompose \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 66b01d7..b86273d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,8 +1,6 @@ - - - + \ No newline at end of file diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000..4ea72a9 --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml new file mode 100644 index 0000000..7ef04e2 --- /dev/null +++ b/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000..8648f94 --- /dev/null +++ b/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 30cdd63..0bdd9c5 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,21 +4,14 @@ - - - - - - - - - - diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 910c7a2..c4a282e 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -52,6 +52,9 @@ + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 4226042..0fc2901 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - + diff --git a/README.md b/README.md index 69aad03..98cbea4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ It's an app built with [Kotlin][1] that shows how to perform CRUD operations in Below you can find the docs for each tehnology that is used in this app: ## Firebase Products: -* [Firebase Authentication][2] +* [Firebase Authentication](https://firebase.google.com/docs/auth) ## Android Architecture Components: * [ViewModel][5] @@ -56,6 +56,7 @@ The code in this project is licensed under the Apache License 2.0. * This is not an officially supported Google product. [1]: https://kotlinlang.org/ +[2]: https://firebase.google.com/docs/auth [3]: https://developer.android.com/topic/libraries/architecture [5]: https://developer.android.com/topic/libraries/architecture/viewmodel [6]: https://developer.android.com/training/dependency-injection/hilt-android @@ -65,3 +66,104 @@ The code in this project is licensed under the Apache License 2.0. [10]: https://medium.com/firebase-tips-tricks/how-to-read-data-from-room-using-kotlin-flow-in-jetpack-compose-7a720dec35f5 [12]: https://developer.android.com/guide/navigation [13]: https://developer.android.com/training/data-storage/room + +--- + +## Guía rápida: uso de `BaseScreen` + +`BaseScreen` centraliza el layout de pantallas en Compose y expone slots para que agregues tus propios componentes sin repetir estructura. + +- Props principales: + - `title: String?` – título simple para el TopBar (si no pasás `topBar`). + - `topBar: @Composable (() -> Unit)?` – TopBar custom (usa `AppTopBar` o el que quieras). + - `header: @Composable (() -> Unit)?` – Cabecera opcional sobre el contenido (chips, filtros, etc.). + - `content: @Composable (PaddingValues) -> Unit` – Contenido principal. Recibe el padding superior ya calculado. + - `bottomBar: @Composable (() -> Unit)?` – Barra inferior (por ejemplo `BottomNavigationBar`). + - `fab: @Composable (() -> Unit)?` – Floating Action Button opcional. + - `centerContent: Boolean` – Centra vertical y horizontalmente el contenido cuando es `true`. + +- Estilos y comportamiento incorporados: + - Respeta el área segura de status bar para evitar solapes con la cámara/recortes. + - Pinta el fondo con `MaterialTheme.colorScheme.surface` dentro de un contenedor con bordes superiores curvos. + - Mantiene paddings y radios desde `ui.theme.Dimens`. + +- Ejemplo mínimo: + +```kotlin +@Composable +fun ExampleScreen() { + BaseScreen( + title = "Example", + bottomBar = { BottomNavigationBar() }, + ) { _ -> + // Tu contenido + Column(Modifier.fillMaxSize().padding(Dimens.paddingLarge)) { + Text("Hola BaseScreen") + } + } +} +``` + +- Ejemplo con TopBar custom + Header + FAB: + +```kotlin +@Composable +fun WithHeaderAndFab() { + BaseScreen( + topBar = { AppTopBar(title = "Dashboard") }, + header = { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { + FilterChip(label = "All") + FilterChip(label = "Income") + FilterChip(label = "Expense") + } + }, + fab = { AddActionFab(onClick = { /* do something */ }) }, + bottomBar = { BottomNavigationBar() } + ) { innerPadding -> + LazyColumn(contentPadding = innerPadding) { + items(100) { index -> Text("Item #$index") } + } + } +} +``` + +### Ejemplo centrado (empty state) + +```kotlin +@Composable +fun EmptyState() { + BaseScreen(title = "No Data", centerContent = true) { _ -> + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon(Icons.Default.Info, contentDescription = null) + Spacer(Modifier.height(8.dp)) + Text("Todavía no hay elementos") + } + } +} +``` + +### Ejemplo con lista seccionada (como Notification) + +```kotlin +@Composable +fun SectionList(sections: List
) { + BaseScreen(title = "Sectioned") { _ -> + LazyColumn( + modifier = Modifier.fillMaxSize().padding(Dimens.paddingMedium), + verticalArrangement = Arrangement.spacedBy(Dimens.paddingMedium), + contentPadding = PaddingValues(bottom = Dimens.paddingLarge) + ) { + sections.forEach { section -> + item(section.title) { Text(section.title) } + items(section.items) { item -> ItemRow(item) } + } + } + } +} +``` + +- Recomendaciones: + - Cuando uses `LazyColumn`, pasá `contentPadding = PaddingValues(bottom = Dimens.paddingLarge)` si tenés navbar para evitar que el último ítem quede tapado. + - Si ves que el título se corta por la cámara/notch, el `BaseScreen` ya aplica `statusBarsPadding()` al TopBar. + - Para pantallas centradas (empty states, loaders): `centerContent = true`. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 110a8c3..7a54de1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,11 +46,21 @@ dependencies { implementation(libs.hilt.navigation.compose) //Hilt implementation(libs.hilt) + implementation(libs.material3) + implementation(libs.foundation) + implementation(libs.ui) + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.runtime) ksp(libs.hilt.compiler) //Room implementation(libs.room.runtime) implementation(libs.room.ktx) ksp(libs.room.compiler) + //Coil + implementation(libs.coil.compose) + // Retrofit + implementation(libs.retrofit) + implementation(libs.converter.gson) //Serialization implementation(libs.serialization) //Tests diff --git a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/HiltTestRunner.kt b/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/HiltTestRunner.kt deleted file mode 100644 index 18f5ebc..0000000 --- a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/HiltTestRunner.kt +++ /dev/null @@ -1,16 +0,0 @@ -package ro.alexmamo.roomjetpackcompose - -import android.app.Application -import android.content.Context -import androidx.test.runner.AndroidJUnitRunner -import dagger.hilt.android.testing.HiltTestApplication - -class HiltTestRunner : AndroidJUnitRunner() { - override fun newApplication( - cl: ClassLoader?, - className: String?, - context: Context? - ): Application { - return super.newApplication(cl, HiltTestApplication::class.java.name, context) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/data/dao/BookDaoTest.kt b/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/data/dao/BookDaoTest.kt deleted file mode 100644 index ed8202e..0000000 --- a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/data/dao/BookDaoTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.data.dao - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import ro.alexmamo.roomjetpackcompose.data.network.BookDb -import ro.alexmamo.roomjetpackcompose.utils.getBookTest -import ro.alexmamo.roomjetpackcompose.utils.getUpdatedBookTest -import java.io.IOException -import javax.inject.Inject - -@HiltAndroidTest -class BookDaoTest() { - @get:Rule - var hiltRule = HiltAndroidRule(this) - - @Inject - lateinit var bookDao: BookDao - @Inject - lateinit var bookDb: BookDb - - val context = ApplicationProvider.getApplicationContext() - private val bookTest = getBookTest(context) - private val updatedBookTest = getUpdatedBookTest(context) - - @Before - fun init() { - hiltRule.inject() - } - - @Test - @Throws(Exception::class) - fun testInsertAndGetBookById() = runTest { - bookDao.insertBook(bookTest) - val book = bookDao.getBookById(bookTest.id) - Truth.assertThat(book).isEqualTo(bookTest) - } - - @Test - @Throws(Exception::class) - fun testInsertAndCheckIfBookExistsInBookList() = runTest { - bookDao.insertBook(bookTest) - val bookList = bookDao.getBookList().first() - Truth.assertThat(bookTest).isIn(bookList) - } - - @Test - @Throws(Exception::class) - fun testInsertAndCheckTheSizeOfBookList() = runTest { - bookDao.insertBook(bookTest) - val bookList = bookDao.getBookList().first() - Truth.assertThat(bookList.size).isEqualTo(1) - } - - @Test - @Throws(Exception::class) - fun testUpdateAndGetBookById() = runTest { - bookDao.insertBook(bookTest) - bookDao.updateBook(updatedBookTest) - val book = bookDao.getBookById(bookTest.id) - Truth.assertThat(book.title).isEqualTo(updatedBookTest.title) - } - - @Test - @Throws(Exception::class) - fun testInsertAndDeleteAndCheckTheSizeOfBookList() = runTest { - bookDao.insertBook(bookTest) - bookDao.deleteBook(bookTest) - val bookList = bookDao.getBookList().first() - Truth.assertThat(bookList).isEmpty() - } - - @After - @Throws(IOException::class) - fun closeDb() { - bookDb.close() - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/data/repository/FakeBookRepositoryImpl.kt b/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/data/repository/FakeBookRepositoryImpl.kt deleted file mode 100644 index 4d3f459..0000000 --- a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/data/repository/FakeBookRepositoryImpl.kt +++ /dev/null @@ -1,34 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.data.repository - -import kotlinx.coroutines.flow.flow -import ro.alexmamo.roomjetpackcompose.domain.model.Book -import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository - -class FakeBookRepositoryImpl() : BookRepository { - private val bookList = mutableListOf() - - override fun getBookList() = flow { - emit(bookList) - } - - override suspend fun getBookById(id: Int) = bookList.find { book -> - book.id == id - } - - override suspend fun insertBook(book: Book) { - bookList.add(book) - } - - override suspend fun updateBook(book: Book) { - val indexOfFirstBook = bookList.indexOfFirst { firstBook -> - firstBook.id == book.id - } - if (indexOfFirstBook != -1) { - bookList[indexOfFirstBook] = book - } - } - - override suspend fun deleteBook(book: Book) { - bookList.remove(book) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/di/AppModuleTest.kt b/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/di/AppModuleTest.kt deleted file mode 100644 index fd52429..0000000 --- a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/di/AppModuleTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.di - -import android.content.Context -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.components.SingletonComponent -import dagger.hilt.testing.TestInstallIn -import ro.alexmamo.roomjetpackcompose.data.network.BookDb -import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository -import ro.alexmamo.roomjetpackcompose.presentation.book_list.BookListViewModel -import ro.alexmamo.roomjetpackcompose.data.repository.FakeBookRepositoryImpl - -@Module -@TestInstallIn( - components = [SingletonComponent::class], - replaces = [AppModule::class] -) -class AppModuleTest { - @Provides - fun provideBookDb() = Room.inMemoryDatabaseBuilder( - ApplicationProvider.getApplicationContext(), - BookDb::class.java - ).build() - - @Provides - fun provideBookDao( - bookDb: BookDb - ) = bookDb.bookDao - - @Provides - fun provideBookRepository(): BookRepository = FakeBookRepositoryImpl() - - @Provides - fun provideBookListViewModel( - repo: BookRepository - ) = BookListViewModel(repo) -} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/domain/BookRepositoryTest.kt b/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/domain/BookRepositoryTest.kt deleted file mode 100644 index fd7511c..0000000 --- a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/domain/BookRepositoryTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.domain - -import android.content.Context -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository -import ro.alexmamo.roomjetpackcompose.utils.getBookTest -import ro.alexmamo.roomjetpackcompose.utils.getUpdatedBookTest -import javax.inject.Inject - -@HiltAndroidTest -class BookRepositoryTest { - @get:Rule(order = 0) - var hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 1) - val composeTestRule = createAndroidComposeRule() - - @Inject - lateinit var fakeRepo: BookRepository - - val context = ApplicationProvider.getApplicationContext() - private val bookTest = getBookTest(context) - private val updatedBookTest = getUpdatedBookTest(context) - - @Before - fun init() { - hiltRule.inject() - } - - @Test - fun testInsertAndGetBookById() = runBlocking { - fakeRepo.insertBook(bookTest) - val book = fakeRepo.getBookById(bookTest.id) - Truth.assertThat(book).isEqualTo(bookTest) - } - - @Test - fun testInsertAndCheckIfBookExistsInBookList() = runBlocking { - fakeRepo.insertBook(bookTest) - val bookList = fakeRepo.getBookList().first() - Truth.assertThat(bookTest).isIn(bookList) - } - - @Test - fun testInsertAndCheckTheSizeOfBookList() = runTest { - fakeRepo.insertBook(bookTest) - val bookList = fakeRepo.getBookList().first() - Truth.assertThat(bookList.size).isEqualTo(1) - } - - @Test - fun testUpdateAndGetBookById() = runTest { - fakeRepo.insertBook(bookTest) - fakeRepo.updateBook(updatedBookTest) - val book = fakeRepo.getBookById(bookTest.id) - Truth.assertThat(book?.title).isEqualTo(updatedBookTest.title) - } - - @Test - @Throws(Exception::class) - fun testInsertAndDeleteAndCheckTheSizeOfBookList() = runTest { - fakeRepo.insertBook(bookTest) - fakeRepo.deleteBook(bookTest) - val bookList = fakeRepo.getBookList().first() - Truth.assertThat(bookList).isEmpty() - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/navigation/BookNavigationTest.kt b/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/navigation/BookNavigationTest.kt deleted file mode 100644 index f8b7f60..0000000 --- a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/navigation/BookNavigationTest.kt +++ /dev/null @@ -1,130 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.navigation - -import android.content.Context -import androidx.activity.compose.setContent -import androidx.annotation.StringRes -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.navigation.compose.ComposeNavigator -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.testing.TestNavHostController -import androidx.navigation.toRoute -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import ro.alexmamo.roomjetpackcompose.R -import ro.alexmamo.roomjetpackcompose.domain.model.toBookDetails -import ro.alexmamo.roomjetpackcompose.presentation.MainActivity -import ro.alexmamo.roomjetpackcompose.presentation.book_list.BookListScreen -import ro.alexmamo.roomjetpackcompose.presentation.book_list.BookListViewModel -import ro.alexmamo.roomjetpackcompose.presentation.book_details.BookDetailsScreen -import ro.alexmamo.roomjetpackcompose.utils.getBookTest -import javax.inject.Inject - -@HiltAndroidTest -class BookNavigationTest { - @get:Rule(order = 0) - var hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 1) - val composeTestRule = createAndroidComposeRule() - - @Inject - lateinit var fakeViewModel: BookListViewModel - - val context = ApplicationProvider.getApplicationContext() - private val bookTest = getBookTest(context) - - lateinit var navController: TestNavHostController - - @Before - fun init() { - hiltRule.inject() - } - - @Before - fun setupNavHost() { - composeTestRule.activity.setContent { - navController = TestNavHostController(LocalContext.current) - navController.navigatorProvider.addNavigator(ComposeNavigator()) - fakeViewModel.insertBook(bookTest) - - NavHost( - navController = navController, - startDestination = BookListScreen - ) { - composable { - BookListScreen( - viewModel = fakeViewModel, - navigateToBookDetailsScreen = { book -> - val bookDetails = book.toBookDetails() - navController.navigate(bookDetails) - } - ) - } - composable { entry -> - val bookDetails = entry.toRoute() - val book = bookDetails.toBook() - BookDetailsScreen( - book = book, - navigateBack = navController::navigateUp - ) - } - } - } - composeTestRule.waitForIdle() - } - - @Test - fun testStartDestinationByRoute() { - val startDestination = navController.graph.startDestinationRoute - val currentDestination = navController.currentBackStackEntry?.destination?.route - Truth.assertThat(currentDestination).isEqualTo(startDestination) - } - - @Test - fun testStartDestinationByText() { - composeTestRule - .onNodeWithText(getString(R.string.book_list_screen_title)) - .assertIsDisplayed() - } - - @Test - fun testNavigationFromBookListScreenToBookDetailsScreen() { - composeTestRule.apply { - onNodeWithText(getString(R.string.book_list_screen_title)) - .assertIsDisplayed() - onNodeWithText(bookTest.title) - .performClick() - onNodeWithText(getString(R.string.book_details_screen_title)) - .assertIsDisplayed() - } - } - - @Test - fun testNavigationFromBookListScreenToBookDetailsScreenAndBack() { - composeTestRule.apply { - onNodeWithText(getString(R.string.book_list_screen_title)) - .assertIsDisplayed() - onNodeWithText(bookTest.title) - .performClick() - onNodeWithText(getString(R.string.book_details_screen_title)) - .assertIsDisplayed() - onNodeWithContentDescription(getString(R.string.navigate_back)) - .performClick() - onNodeWithText(getString(R.string.book_list_screen_title)) - .assertIsDisplayed() - } - } - - private fun getString(@StringRes resId: Int) = composeTestRule.activity.getString(resId) -} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListScreenTest.kt b/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListScreenTest.kt deleted file mode 100644 index b07168c..0000000 --- a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListScreenTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list - -import android.content.Context -import androidx.annotation.StringRes -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput -import androidx.test.core.app.ApplicationProvider -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Rule -import org.junit.Test -import ro.alexmamo.roomjetpackcompose.R -import ro.alexmamo.roomjetpackcompose.presentation.MainActivity -import ro.alexmamo.roomjetpackcompose.utils.getBookTest - -@HiltAndroidTest -class BookListScreenTest { - @get:Rule(order = 0) - var hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 1) - val composeTestRule = createAndroidComposeRule() - - val context = ApplicationProvider.getApplicationContext() - private val bookTest = getBookTest(context) - - @Test - fun testBookClickAndNavigationToBookDetailsScreenAndBackToBookListScreen() { - composeTestRule.apply { - onNodeWithContentDescription(getString(R.string.open_insert_book_dialog)) - .performClick() - onNodeWithText(getString(R.string.book_title)) - .performTextInput(bookTest.title) - onNodeWithText(getString(R.string.book_author)) - .performTextInput(bookTest.author) - onNodeWithText(getString(R.string.insert_button)) - .performClick() - onNodeWithText(bookTest.title) - .performClick() - onNodeWithText(getString(R.string.book_details_screen_title)) - .assertIsDisplayed() - onNodeWithText(bookTest.title) - .assertIsDisplayed() - onNodeWithText("by ${bookTest.author}") - .assertIsDisplayed() - onNodeWithContentDescription(getString(R.string.navigate_back)) - .performClick() - onNodeWithText(getString(R.string.book_list_screen_title)) - .assertIsDisplayed() - } - } - - private fun getString(@StringRes resId: Int) = composeTestRule.activity.getString(resId) -} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListContentTest.kt b/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListContentTest.kt deleted file mode 100644 index c03501b..0000000 --- a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListContentTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components - -import androidx.annotation.StringRes -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithText -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Rule -import org.junit.Test -import ro.alexmamo.roomjetpackcompose.R -import ro.alexmamo.roomjetpackcompose.presentation.MainActivity - -@HiltAndroidTest -class BookListContentTest { - @get:Rule(order = 0) - var hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 1) - val composeTestRule = createAndroidComposeRule() - - @Test - fun testBookListContent() { - composeTestRule.apply { - onNodeWithText(getString(R.string.empty_book_list_text)) - .assertIsDisplayed() - } - } - - private fun getString(@StringRes resId: Int) = composeTestRule.activity.getString(resId) -} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListTopBarTest.kt b/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListTopBarTest.kt deleted file mode 100644 index a7a8659..0000000 --- a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListTopBarTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components - -import androidx.annotation.StringRes -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithText -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Rule -import org.junit.Test -import ro.alexmamo.roomjetpackcompose.R -import ro.alexmamo.roomjetpackcompose.presentation.MainActivity - -@HiltAndroidTest -class BookListTopBarTest { - @get:Rule(order = 0) - var hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 1) - val composeTestRule = createAndroidComposeRule() - - @Test - fun testBookListTopBar() { - composeTestRule.apply { - onNodeWithText(getString(R.string.book_list_screen_title)) - .assertIsDisplayed() - } - } - - private fun getString(@StringRes resId: Int) = composeTestRule.activity.getString(resId) -} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookAlertDialogTest.kt b/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookAlertDialogTest.kt deleted file mode 100644 index 80059a6..0000000 --- a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookAlertDialogTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components - -import androidx.annotation.StringRes -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Rule -import org.junit.Test -import ro.alexmamo.roomjetpackcompose.R -import ro.alexmamo.roomjetpackcompose.presentation.MainActivity - -@HiltAndroidTest -class InsertBookAlertDialogTest { - @get:Rule(order = 0) - var hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 1) - val composeTestRule = createAndroidComposeRule() - - @Test - fun testInsertBookFloatingActionButton() { - composeTestRule.apply { - onNodeWithContentDescription(getString(R.string.open_insert_book_dialog)) - .assertIsDisplayed() - } - } - - private fun getString(@StringRes resId: Int) = composeTestRule.activity.getString(resId) -} \ No newline at end of file diff --git a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/utils/Utils.kt b/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/utils/Utils.kt deleted file mode 100644 index a85bbb9..0000000 --- a/app/src/androidTest/java/ro/alexmamo/roomjetpackcompose/utils/Utils.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.utils - -import android.content.Context -import ro.alexmamo.roomjetpackcompose.R -import ro.alexmamo.roomjetpackcompose.domain.model.Book - -fun getBookTest(context: Context): Book { - return Book( - id = 1, - title = context.getString(R.string.title_test), - author = context.getString(R.string.author_test) - ) -} - -fun getUpdatedBookTest(context: Context): Book { - return getBookTest(context).copy( - title = context.getString(R.string.new_title_test) - ) -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2ea2c7c..57bd175 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + Unit, - imageVector: ImageVector, - resourceId: Int + modifier: Modifier = Modifier, + withCircle: Boolean = false, + circleSize: Dp = 30.dp, + circleColor: Color = MaterialTheme.colorScheme.surface, + iconSizeWhenCircle: Dp = 18.dp, + iconSizeDefault: Dp = 19.dp, + content: @Composable (Modifier) -> Unit ) { IconButton( - onClick = onActionIconButtonClick + onClick = onActionIconButtonClick, + modifier = modifier ) { - Icon( - imageVector = imageVector, - contentDescription = stringResource( - id = resourceId - ) - ) + val iconModifier = if (withCircle) Modifier.size(iconSizeWhenCircle) else Modifier.size(iconSizeDefault) + if (withCircle) { + Box( + modifier = Modifier + .size(circleSize) + .background(color = circleColor, shape = CircleShape), + contentAlignment = Alignment.Center + ) { + content(iconModifier) + } + } else { + content(iconModifier) + } } } \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/BottomNavigationBar.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/BottomNavigationBar.kt new file mode 100644 index 0000000..a6e3890 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/BottomNavigationBar.kt @@ -0,0 +1,58 @@ +package ro.alexmamo.roomjetpackcompose.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.runtime.* +import androidx.compose.ui.unit.dp +import ro.alexmamo.roomjetpackcompose.R + +@Composable +fun BottomNavigationBar() { + var selectedIndex by remember { mutableStateOf(0) } + + val icons = listOf( + R.drawable.home, + R.drawable.analysis, + R.drawable.transactions, + R.drawable.expenses, + R.drawable.profile + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(100.dp) + .background( + color = MaterialTheme.colorScheme.surface, // o background, según prefieras + shape = RoundedCornerShape(topStart = 70.dp, topEnd = 70.dp) + ), + contentAlignment = Alignment.Center + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + icons.forEachIndexed { index, icon -> + NavigationMenuButton( + iconRes = icon, + selected = index == selectedIndex, + onClick = { selectedIndex = index } + ) + } + } + } +} + + diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/ButtonCategoriesMenuPrimaryAndSecondary.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/ButtonCategoriesMenuPrimaryAndSecondary.kt new file mode 100644 index 0000000..e9c7c70 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/ButtonCategoriesMenuPrimaryAndSecondary.kt @@ -0,0 +1,43 @@ +package ro.alexmamo.roomjetpackcompose.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import ro.alexmamo.roomjetpackcompose.ui.theme.LightBlue +import ro.alexmamo.roomjetpackcompose.ui.theme.OceanBlue + +@Composable +fun ButtonCategoriesMenuPrimaryAndSecondary( + iconRes: Int, + selected: Boolean, + onClick: () -> Unit +) { + val backgroundColor = if (selected) OceanBlue else LightBlue + val iconTint = Color.White + + Box( + modifier = Modifier + .width(105.dp) + .height(98.dp) + .clip(RoundedCornerShape(26.dp)) + .background(backgroundColor) + .clickable { onClick() }, + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = iconTint, + modifier = Modifier.size(45.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/ButtonsGreen.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/ButtonsGreen.kt new file mode 100644 index 0000000..5c66063 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/ButtonsGreen.kt @@ -0,0 +1,54 @@ +package ro.alexmamo.roomjetpackcompose.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ro.alexmamo.roomjetpackcompose.ui.theme.CaribbeanGreen +import ro.alexmamo.roomjetpackcompose.ui.theme.LightGreen +import ro.alexmamo.roomjetpackcompose.ui.theme.Void + +enum class ButtonGreenType { + DARK, LIGHT +} + +@Composable +fun ButtonsGreen( + text: String, + type: ButtonGreenType, + onClick: () -> Unit +) { + val backgroundColor = when (type) { + ButtonGreenType.DARK -> CaribbeanGreen + ButtonGreenType.LIGHT -> LightGreen + } + + val textColor = Void + + Box( + modifier = Modifier + .width(207.dp) + .height(45.dp) + .clip(RoundedCornerShape(28.dp)) + .background(backgroundColor) + .clickable { onClick() }, + contentAlignment = Alignment.Center + ) { + Text( + text = text, + color = textColor, + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/EyePassButton.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/EyePassButton.kt new file mode 100644 index 0000000..d4ffc41 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/EyePassButton.kt @@ -0,0 +1,40 @@ +package ro.alexmamo.roomjetpackcompose.components + + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import ro.alexmamo.roomjetpackcompose.R +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import ro.alexmamo.roomjetpackcompose.ui.theme.Void + + +@Composable +fun EyesPassButton( + passwordVisible: Boolean, + onToggleVisibility: () -> Unit +) { + val iconRes = if (passwordVisible) R.drawable.eye_on else R.drawable.eye_off + + Box( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(50.dp)) + .clickable { onToggleVisibility() }, + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = if (passwordVisible) "Ocultar contraseña" else "Mostrar contraseña", + tint = Void, + modifier = Modifier.size(28.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/LoadingIndicator.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/LoadingIndicator.kt index 07be205..6cab307 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/LoadingIndicator.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/LoadingIndicator.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier @Composable fun LoadingIndicator() { + // ESTO ESTA PENDIENTE AUN Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/MenuSwitchOnOff.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/MenuSwitchOnOff.kt new file mode 100644 index 0000000..405df26 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/MenuSwitchOnOff.kt @@ -0,0 +1,61 @@ +package ro.alexmamo.roomjetpackcompose.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ro.alexmamo.roomjetpackcompose.ui.theme.CaribbeanGreen + +@Composable +fun MenuSwitchOnOff( + options: List, + selectedIndex: Int?, + onOptionSelected: (Int) -> Unit +) { + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background(color = MaterialTheme.colorScheme.surface, RoundedCornerShape(22.dp)) + .padding(horizontal = 8.dp, vertical = 6.dp) + ) { + options.forEachIndexed { index, label -> + val isSelected = index == selectedIndex + val backgroundColor = if (isSelected) CaribbeanGreen else + MaterialTheme.colorScheme.surface + val textColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else + MaterialTheme.colorScheme.onSecondary + + Box( + modifier = Modifier + .weight(1f) + .height(60.dp) + .clip(RoundedCornerShape(22.dp)) + .background(backgroundColor) + .clickable { onOptionSelected(index) } + .padding(vertical = 10.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = label, + color = textColor, + fontSize = 15.sp, + fontWeight = FontWeight.Bold + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/NavigationMenu.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/NavigationMenu.kt new file mode 100644 index 0000000..0bf7714 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/NavigationMenu.kt @@ -0,0 +1,50 @@ +package ro.alexmamo.roomjetpackcompose.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import ro.alexmamo.roomjetpackcompose.ui.theme.CaribbeanGreen +import ro.alexmamo.roomjetpackcompose.ui.theme.LightGreen +import ro.alexmamo.roomjetpackcompose.ui.theme.Void + +@Composable +fun NavigationMenuButton( + iconRes: Int, + selected: Boolean, + onClick: () -> Unit +) { + val backgroundColor = if (selected) CaribbeanGreen else MaterialTheme.colorScheme.surface + val iconTint = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSecondary + + + + Box( + modifier = Modifier + .height(53.dp) + .width(57.dp) + .clip(RoundedCornerShape(22.dp)) + .background(backgroundColor) + .clickable { onClick() }, + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = iconTint, + modifier = Modifier.size(28.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/NotificationItem.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/NotificationItem.kt new file mode 100644 index 0000000..cf6d73a --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/NotificationItem.kt @@ -0,0 +1,90 @@ +package ro.alexmamo.roomjetpackcompose.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.text.font.FontWeight +import ro.alexmamo.roomjetpackcompose.ui.theme.CaribbeanGreen +import ro.alexmamo.roomjetpackcompose.ui.theme.Dimens +import ro.alexmamo.roomjetpackcompose.ui.theme.Cyprus + +@Composable +fun NotificationItem( + iconRes: Int, + titleRes: Int, + messageRes: Int, + timeRes: Int, + modifier: Modifier = Modifier +) { + val title = stringResource(id = titleRes) + val message = stringResource(id = messageRes) + val time = stringResource(id = timeRes) + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = Dimens.paddingSmall), + // alineamos al top para que icon coincida con la línea del título + verticalAlignment = Alignment.Top + ) { + // usar surface como fondo del contenedor del icon para mejor contraste + Box( + modifier = Modifier + .size(44.dp) + .clip(RoundedCornerShape(12.dp)) + .background(CaribbeanGreen), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = Cyprus, + modifier = Modifier.size(24.dp) + ) + } + + Spacer(modifier = Modifier.width(Dimens.paddingMedium)) + + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy(fontSize = 15.sp, fontWeight = FontWeight.Medium), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = message, + style = MaterialTheme.typography.bodySmall.copy(fontSize = 14.sp), + color = MaterialTheme.colorScheme.onSecondary, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = time, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.End + ) + Spacer(modifier = Modifier.height(4.dp)) + } + } +} diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/TopBar.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/TopBar.kt new file mode 100644 index 0000000..2da35c2 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/components/TopBar.kt @@ -0,0 +1,43 @@ +package ro.alexmamo.roomjetpackcompose.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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 ro.alexmamo.roomjetpackcompose.ui.theme.Dimens + +@Composable +fun AppTopBar( + modifier: Modifier = Modifier, + title: String? = null, + leftAction: (@Composable () -> Unit)? = null, + rightAction: (@Composable () -> Unit)? = null +) { + Box( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = Dimens.paddingLarge, vertical = Dimens.paddingSmall) + ) { + if (leftAction != null) { + Box(modifier = Modifier.align(Alignment.CenterStart)) { + leftAction() + } + } + + if (title != null) { + Box(modifier = Modifier.align(Alignment.Center)) { + Text(text = title, style = MaterialTheme.typography.titleLarge) + } + } + + if (rightAction != null) { + Box(modifier = Modifier.align(Alignment.CenterEnd)) { + rightAction() + } + } + } +} diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/core/Utils.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/core/Utils.kt index 197e1f0..c1c80b2 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/core/Utils.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/core/Utils.kt @@ -9,9 +9,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch const val TAG = "AppTag" -const val BOOK_TABLE = "book_table" -const val AUTHOR_FIELD = "author" -const val TITLE_FIELD = "title" + +//const val TAG = "AppTag" +const val TODO_TABLE = "todo_table" +const val NAME_FIELD = "name" +const val DESCRIPTION_FIELD = "description" fun logMessage( message: String diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/dao/BookDao.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/dao/BookDao.kt deleted file mode 100644 index 38e20d2..0000000 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/dao/BookDao.kt +++ /dev/null @@ -1,25 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.data.dao - -import androidx.room.* -import androidx.room.OnConflictStrategy.Companion.IGNORE -import kotlinx.coroutines.flow.Flow -import ro.alexmamo.roomjetpackcompose.core.BOOK_TABLE -import ro.alexmamo.roomjetpackcompose.domain.model.Book - -@Dao -interface BookDao { - @Query("SELECT * FROM $BOOK_TABLE ORDER BY id ASC") - fun getBookList(): Flow> - - @Query("SELECT * FROM $BOOK_TABLE WHERE id = :id") - suspend fun getBookById(id: Int): Book - - @Insert(onConflict = IGNORE) - suspend fun insertBook(book: Book) - - @Update - suspend fun updateBook(book: Book) - - @Delete - suspend fun deleteBook(book: Book) -} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/dao/TodoDao.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/dao/TodoDao.kt new file mode 100644 index 0000000..f9d5b7e --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/dao/TodoDao.kt @@ -0,0 +1,29 @@ +package ro.alexmamo.roomjetpackcompose.data.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy.Companion.IGNORE +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow +import ro.alexmamo.roomjetpackcompose.core.TODO_TABLE +import ro.alexmamo.roomjetpackcompose.domain.model.Todo + +@Dao +interface TodoDao { + @Query("SELECT * FROM $TODO_TABLE ORDER BY id ASC") + fun getTodoList(): Flow> + + @Query("SELECT * FROM $TODO_TABLE WHERE id = :id") + suspend fun getTodoById(id: Int): Todo + + @Insert(onConflict = IGNORE) + suspend fun insertTodo(todo: Todo) + + @Update + suspend fun updateTodo(todo: Todo) + + @Delete + suspend fun deleteTodo(todo: Todo) +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/network/BookDb.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/network/BookDb.kt deleted file mode 100644 index 94c0738..0000000 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/network/BookDb.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.data.network - -import androidx.room.Database -import androidx.room.RoomDatabase -import ro.alexmamo.roomjetpackcompose.data.dao.BookDao -import ro.alexmamo.roomjetpackcompose.domain.model.Book - -@Database( - entities = [Book::class], - version = 1, - exportSchema = false -) -abstract class BookDb : RoomDatabase() { - abstract val bookDao: BookDao -} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/network/TodoDb.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/network/TodoDb.kt new file mode 100644 index 0000000..3cba476 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/network/TodoDb.kt @@ -0,0 +1,15 @@ +package ro.alexmamo.roomjetpackcompose.data.network + +import androidx.room.Database +import androidx.room.RoomDatabase +import ro.alexmamo.roomjetpackcompose.data.dao.TodoDao +import ro.alexmamo.roomjetpackcompose.domain.model.Todo + +@Database( + entities = [Todo::class], + version = 3, + exportSchema = false +) +abstract class TodoDb : RoomDatabase() { + abstract val todoDao: TodoDao +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/repository/BookRepositoryImpl.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/repository/BookRepositoryImpl.kt deleted file mode 100644 index a1e053a..0000000 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/repository/BookRepositoryImpl.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.data.repository - -import ro.alexmamo.roomjetpackcompose.data.dao.BookDao -import ro.alexmamo.roomjetpackcompose.domain.model.Book -import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository - -class BookRepositoryImpl( - private val bookDao: BookDao -) : BookRepository { - override fun getBookList() = bookDao.getBookList() - - override suspend fun getBookById(id: Int) = bookDao.getBookById(id) - - override suspend fun insertBook(book: Book) = bookDao.insertBook(book) - - override suspend fun updateBook(book: Book) = bookDao.updateBook(book) - - override suspend fun deleteBook(book: Book) = bookDao.deleteBook(book) -} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/repository/TodoRepositoryImpl.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/repository/TodoRepositoryImpl.kt new file mode 100644 index 0000000..b01009d --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/data/repository/TodoRepositoryImpl.kt @@ -0,0 +1,19 @@ +package ro.alexmamo.roomjetpackcompose.data.repository + +import ro.alexmamo.roomjetpackcompose.data.dao.TodoDao +import ro.alexmamo.roomjetpackcompose.domain.model.Todo +import ro.alexmamo.roomjetpackcompose.domain.repository.TodoRepository + +class TodoRepositoryImpl( + private val todoDao: TodoDao +) : TodoRepository { + override fun getTodoList() = todoDao.getTodoList() + + override suspend fun getTodoById(id: Int) = todoDao.getTodoById(id) + + override suspend fun insertTodo(todo: Todo) = todoDao.insertTodo(todo) + + override suspend fun updateTodo(todo: Todo) = todoDao.updateTodo(todo) + + override suspend fun deleteTodo(todo: Todo) = todoDao.deleteTodo(todo) +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/di/AppModule.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/di/AppModule.kt index 54a61fe..331bb6a 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/di/AppModule.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/di/AppModule.kt @@ -8,33 +8,35 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import ro.alexmamo.roomjetpackcompose.R -import ro.alexmamo.roomjetpackcompose.data.dao.BookDao -import ro.alexmamo.roomjetpackcompose.data.network.BookDb -import ro.alexmamo.roomjetpackcompose.data.repository.BookRepositoryImpl -import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository +import ro.alexmamo.roomjetpackcompose.data.dao.TodoDao +import ro.alexmamo.roomjetpackcompose.data.network.TodoDb +import ro.alexmamo.roomjetpackcompose.data.repository.TodoRepositoryImpl +import ro.alexmamo.roomjetpackcompose.domain.model.Todo +import ro.alexmamo.roomjetpackcompose.domain.repository.TodoRepository @Module @InstallIn(SingletonComponent::class) class AppModule { @Provides - fun provideBookDb( + fun provideTodoDb( @ApplicationContext context: Context ) = Room.databaseBuilder( context, - BookDb::class.java, + TodoDb::class.java, context.resources.getString(R.string.db_name) - ).build() + ).fallbackToDestructiveMigration().build() + @Provides - fun provideBookDao( - bookDb: BookDb - ) = bookDb.bookDao + fun provideTodoDao( + todoDb: TodoDb + ) = todoDb.todoDao @Provides - fun provideBookRepository( - bookDao: BookDao - ): BookRepository = BookRepositoryImpl( - bookDao = bookDao + fun provideTodoRepository( + todoDao: TodoDao + ): TodoRepository = TodoRepositoryImpl( + todoDao = todoDao ) } \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/model/Book.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/model/Book.kt deleted file mode 100644 index 49afea0..0000000 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/model/Book.kt +++ /dev/null @@ -1,20 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.domain.model - -import androidx.room.Entity -import androidx.room.PrimaryKey -import ro.alexmamo.roomjetpackcompose.core.BOOK_TABLE -import ro.alexmamo.roomjetpackcompose.navigation.BookDetails - -@Entity(tableName = BOOK_TABLE) -data class Book( - @PrimaryKey(autoGenerate = true) - val id: Int, - val title: String, - val author: String -) - -fun Book.toBookDetails() = BookDetails( - id = this.id, - title = this.title, - author = this.author -) \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/model/Todo.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/model/Todo.kt new file mode 100644 index 0000000..02042a8 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/model/Todo.kt @@ -0,0 +1,20 @@ +package ro.alexmamo.roomjetpackcompose.domain.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import ro.alexmamo.roomjetpackcompose.core.TODO_TABLE +import ro.alexmamo.roomjetpackcompose.navigation.TodoDetails + +@Entity(tableName = TODO_TABLE) +data class Todo( + @PrimaryKey(autoGenerate = true) + val id: Int, + val name: String, + val description: String +) + +fun Todo.toTodoDetails() = TodoDetails( + id = this.id, + name = this.name, + description = this.description +) \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/repository/BookRepository.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/repository/BookRepository.kt deleted file mode 100644 index e913c92..0000000 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/repository/BookRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.domain.repository - -import kotlinx.coroutines.flow.Flow -import ro.alexmamo.roomjetpackcompose.domain.model.Book - -interface BookRepository { - fun getBookList(): Flow> - - suspend fun getBookById(id: Int): Book? - - suspend fun insertBook(book: Book) - - suspend fun updateBook(book: Book) - - suspend fun deleteBook(book: Book) -} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/repository/TodoRepository.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/repository/TodoRepository.kt new file mode 100644 index 0000000..f509735 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/domain/repository/TodoRepository.kt @@ -0,0 +1,16 @@ +package ro.alexmamo.roomjetpackcompose.domain.repository + +import kotlinx.coroutines.flow.Flow +import ro.alexmamo.roomjetpackcompose.domain.model.Todo + +interface TodoRepository { + fun getTodoList(): Flow> + + suspend fun getTodoById(id: Int): Todo? + + suspend fun insertTodo(todo: Todo) + + suspend fun updateTodo(todo: Todo) + + suspend fun deleteTodo(todo: Todo) +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/ApiResult.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/ApiResult.kt new file mode 100644 index 0000000..25a3807 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/ApiResult.kt @@ -0,0 +1,7 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure + +sealed class ApiResult { + data class Success(val data: T) : ApiResult() + data class Error(val code: Int?, val message: String?) : ApiResult() + data class Exception(val exception: Throwable) : ApiResult() +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/RetrofitUtils.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/RetrofitUtils.kt new file mode 100644 index 0000000..93363fc --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/RetrofitUtils.kt @@ -0,0 +1,22 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure + +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object RetrofitUtils { + val okHttpClient = OkHttpClient.Builder() + .addInterceptor { chain -> + val request = chain.request().newBuilder() + .addHeader("x-api-key", "123456789") + .build() + chain.proceed(request) + } + .build() + + val retrofit: Retrofit = Retrofit.Builder() + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .baseUrl("https://d9811bf4-5e67-4a8c-bdcf-603cbbfc0275.mock.pstmn.io/") + .build() +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/SafeApiCall.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/SafeApiCall.kt new file mode 100644 index 0000000..8aad348 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/SafeApiCall.kt @@ -0,0 +1,31 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure + +import android.util.Log +import retrofit2.Response + +suspend fun safeApiCall( + tag: String = "API_CALL", + apiCall: suspend () -> Response, + map: (T) -> R +): ApiResult { + return try { + val response = apiCall() + if (response.isSuccessful) { + val body = response.body() + if (body != null) { + Log.d(tag, "✅ Éxito: ${response.code()} -> ${body.toString().take(500)}") + ApiResult.Success(map(body)) + } else { + Log.e(tag, "⚠️ Cuerpo nulo en respuesta exitosa (${response.code()})") + ApiResult.Error(response.code(), "Cuerpo de respuesta nulo") + } + } else { + val errorBody = response.errorBody()?.string() + Log.e(tag, "❌ Error HTTP ${response.code()} -> ${errorBody ?: "Sin cuerpo de error"}") + ApiResult.Error(response.code(), errorBody) + } + } catch (e: Exception) { + Log.e(tag, "💥 Excepción: ${e.message}", e) + ApiResult.Exception(e) + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/Auth.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/Auth.kt new file mode 100644 index 0000000..40a24ef --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/Auth.kt @@ -0,0 +1,10 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.auth + +import ro.alexmamo.roomjetpackcompose.infraestructure.user.User + + +interface Auth { + suspend fun login(data: LoginRequest): Token? + + suspend fun createUser(data: CreateUserRequest): User? +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthApi.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthApi.kt new file mode 100644 index 0000000..7d0d349 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthApi.kt @@ -0,0 +1,14 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.auth + +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST +import ro.alexmamo.roomjetpackcompose.infraestructure.user.UserResponse + +interface AuthApi { + @POST("auth/login") + suspend fun login(@Body data: LoginRequest): Response + + @POST("auth/create") + suspend fun createUser(@Body data: CreateUserRequest): Response +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthImpl.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthImpl.kt new file mode 100644 index 0000000..a4f89f7 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthImpl.kt @@ -0,0 +1,28 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.auth + +import ro.alexmamo.roomjetpackcompose.infraestructure.ApiResult +import ro.alexmamo.roomjetpackcompose.infraestructure.RetrofitUtils +import ro.alexmamo.roomjetpackcompose.infraestructure.safeApiCall +import ro.alexmamo.roomjetpackcompose.infraestructure.user.User + +class AuthImpl : Auth { + private val api = RetrofitUtils.retrofit.create(AuthApi::class.java) + + override suspend fun login(data: LoginRequest): Token? { + return when (val result = safeApiCall("login", {api.login(data)}) { res -> res.toModel() + }) { + is ApiResult.Success -> result.data + is ApiResult.Error, + is ApiResult.Exception -> null + } + } + + override suspend fun createUser(data: CreateUserRequest): User? { + return when (val result = safeApiCall("createUser", {api.createUser(data)}) { res -> res.toModel() + }) { + is ApiResult.Success -> result.data + is ApiResult.Error, + is ApiResult.Exception -> null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthModels.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthModels.kt new file mode 100644 index 0000000..2abe342 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthModels.kt @@ -0,0 +1,16 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.auth + +data class Token ( + val token: String, +) + +data class LoginRequest( + val email: String, + val password: String +) + +data class CreateUserRequest( + val email: String, + val password: String, + val username: String +) \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthResponses.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthResponses.kt new file mode 100644 index 0000000..102d677 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/auth/AuthResponses.kt @@ -0,0 +1,12 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.auth + +import com.google.gson.annotations.SerializedName + +data class TokenResponse( + @SerializedName("token") val token: String, +) { + fun toModel(): Token = + Token( + token = token, + ) +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UserApi.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UserApi.kt new file mode 100644 index 0000000..ae8a1bc --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UserApi.kt @@ -0,0 +1,12 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.user + +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +interface UserApi { + @GET("users/{id}") + suspend fun getById(@Path("id") id: Int): Response +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UserModels.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UserModels.kt new file mode 100644 index 0000000..23f1267 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UserModels.kt @@ -0,0 +1,30 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.user + +data class User( + val id: Int, + val email: String, + val username: String, + val password: String, + val name: Name, + val address: Address, + val phone: String, + val __v: Int +) + +data class Name( + val firstname: String, + val lastname: String +) + +data class Address( + val geolocation: Geolocation, + val city: String, + val street: String, + val number: Int, + val zipcode: String +) + +data class Geolocation( + val lat: String, + val long: String +) \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UserResponses.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UserResponses.kt new file mode 100644 index 0000000..5cfa440 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UserResponses.kt @@ -0,0 +1,65 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.user + +import com.google.gson.annotations.SerializedName + +data class UserResponse( + @SerializedName("id") val id: Int, + @SerializedName("email") val email: String, + @SerializedName("username") val username: String, + @SerializedName("password") val password: String, + @SerializedName("name") val name: NameResponse, + @SerializedName("address") val address: AddressResponse, + @SerializedName("phone") val phone: String, + @SerializedName("__v") val v: Int +) { + fun toModel(): User = + User( + id = id, + email = email, + username = username, + password = password, + name = name.toModel(), + address = address.toModel(), + phone = phone, + __v = v + ) +} + +data class NameResponse( + @SerializedName("firstname") val firstname: String, + @SerializedName("lastname") val lastname: String +) { + fun toModel(): Name = + Name( + firstname = firstname, + lastname = lastname + ) +} + +data class AddressResponse( + @SerializedName("geolocation") val geolocation: GeolocationResponse, + @SerializedName("city") val city: String, + @SerializedName("street") val street: String, + @SerializedName("number") val number: Int, + @SerializedName("zipcode") val zipcode: String +) { + fun toModel(): Address = + Address( + geolocation = geolocation.toModel(), + city = city, + street = street, + number = number, + zipcode = zipcode + ) +} + +data class GeolocationResponse( + @SerializedName("lat") val lat: String, + @SerializedName("long") val long: String +) { + fun toModel(): Geolocation = + Geolocation( + lat = lat, + long = long + ) +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/Users.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/Users.kt new file mode 100644 index 0000000..1f82cad --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/Users.kt @@ -0,0 +1,6 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.user + + +interface Users { + suspend fun getById(id: Int): User? +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UsersImpl.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UsersImpl.kt new file mode 100644 index 0000000..931a2d9 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/user/UsersImpl.kt @@ -0,0 +1,21 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.user + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import ro.alexmamo.roomjetpackcompose.infraestructure.ApiResult +import ro.alexmamo.roomjetpackcompose.infraestructure.RetrofitUtils +import ro.alexmamo.roomjetpackcompose.infraestructure.auth.AuthApi +import ro.alexmamo.roomjetpackcompose.infraestructure.safeApiCall + +class UsersImpl : Users { + private val api = RetrofitUtils.retrofit.create(UserApi::class.java) + + override suspend fun getById(id: Int): User? { + return when (val result = safeApiCall("login", {api.getById(id)}) { res -> res.toModel() + }) { + is ApiResult.Success -> result.data + is ApiResult.Error, + is ApiResult.Exception -> null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/Wallet.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/Wallet.kt new file mode 100644 index 0000000..dbccea4 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/Wallet.kt @@ -0,0 +1,5 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.wallet + +interface WalletInterface { + suspend fun get(): Wallet? +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletApi.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletApi.kt new file mode 100644 index 0000000..19d6132 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletApi.kt @@ -0,0 +1,10 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.wallet + +import retrofit2.Response +import retrofit2.http.GET +import ro.alexmamo.roomjetpackcompose.infraestructure.walletimport.WalletResponse + +interface WalletApi { + @GET("transactions") + suspend fun get(): Response +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletImpl.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletImpl.kt new file mode 100644 index 0000000..8839747 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletImpl.kt @@ -0,0 +1,18 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.wallet + +import ro.alexmamo.roomjetpackcompose.infraestructure.ApiResult +import ro.alexmamo.roomjetpackcompose.infraestructure.RetrofitUtils +import ro.alexmamo.roomjetpackcompose.infraestructure.safeApiCall + +class WalletImpl : WalletInterface { + private val api = RetrofitUtils.retrofit.create(WalletApi::class.java) + + override suspend fun get(): Wallet? { + return when (val result = safeApiCall("get", {api.get()}) { res -> res.toModel() + }) { + is ApiResult.Success -> result.data + is ApiResult.Error, + is ApiResult.Exception -> null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletModels.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletModels.kt new file mode 100644 index 0000000..a44d967 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletModels.kt @@ -0,0 +1,19 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.wallet + +data class Wallet( + val userId: Int, + val balance: Double, + val income: Double, + val expense: Double, + val transactions: List +) + +data class Transaction( + val transactionId: String, + val date: String, + val description: String, + val amount: Double, + val currency: String, + val type: String, + val subtype: String +) \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletResponses.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletResponses.kt new file mode 100644 index 0000000..f075732 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/infraestructure/wallet/WalletResponses.kt @@ -0,0 +1,43 @@ +package ro.alexmamo.roomjetpackcompose.infraestructure.walletimport + +import com.google.gson.annotations.SerializedName +import ro.alexmamo.roomjetpackcompose.infraestructure.wallet.Transaction +import ro.alexmamo.roomjetpackcompose.infraestructure.wallet.Wallet + +data class WalletResponse( + @SerializedName("user_id") val userId: Int, + @SerializedName("balance") val balance: Double, + @SerializedName("income") val income: Double, + @SerializedName("expense") val expense: Double, + @SerializedName("transactions") val transactions: List +) { + fun toModel(): Wallet = + Wallet( + userId = userId, + balance = balance, + income = income, + expense = expense, + transactions = transactions.map { it.toModel() } + ) +} + +data class TransactionResponse( + @SerializedName("transaction_id") val transactionId: String, + @SerializedName("date") val date: String, + @SerializedName("description") val description: String, + @SerializedName("amount") val amount: Double, + @SerializedName("currency") val currency: String, + @SerializedName("type") val type: String, + @SerializedName("subtype") val subtype: String +) { + fun toModel(): Transaction = + Transaction( + transactionId = transactionId, + date = date, + description = description, + amount = amount, + currency = currency, + type = type, + subtype = subtype + ) +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/navigation/NavGraph.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/navigation/NavGraph.kt index dfc0cf5..39d962d 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/navigation/NavGraph.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/navigation/NavGraph.kt @@ -5,9 +5,17 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.toRoute -import ro.alexmamo.roomjetpackcompose.domain.model.toBookDetails -import ro.alexmamo.roomjetpackcompose.presentation.book_list.BookListScreen -import ro.alexmamo.roomjetpackcompose.presentation.book_details.BookDetailsScreen +import ro.alexmamo.roomjetpackcompose.domain.model.toTodoDetails +import ro.alexmamo.roomjetpackcompose.presentation.create_user.CreateUserScreen +import ro.alexmamo.roomjetpackcompose.presentation.create_user.CreateUserViewModel +import ro.alexmamo.roomjetpackcompose.presentation.home.HomeScreen +import ro.alexmamo.roomjetpackcompose.presentation.home.WalletViewModel +import ro.alexmamo.roomjetpackcompose.presentation.login.LoginScreen +import ro.alexmamo.roomjetpackcompose.presentation.login.LoginViewModel +import ro.alexmamo.roomjetpackcompose.presentation.profile.ProfileScreen +import ro.alexmamo.roomjetpackcompose.presentation.profile.UserViewModel +import ro.alexmamo.roomjetpackcompose.presentation.todo_details.TodoDetailsScreen +import ro.alexmamo.roomjetpackcompose.presentation.todo_list.TodoListScreen @Composable fun NavGraph( @@ -15,21 +23,44 @@ fun NavGraph( ) { NavHost( navController = navController, - startDestination = BookListScreen + startDestination = HomeScreen ) { - composable { - BookListScreen( - navigateToBookDetailsScreen = { book -> - val bookDetails = book.toBookDetails() - navController.navigate(bookDetails) + + val loginViewModel = LoginViewModel() + val userViewModel = UserViewModel() + val createUserViewModel = CreateUserViewModel() + val walletViewModel = WalletViewModel() + + composable { + TodoListScreen( + navigateToTodoDetailsScreen = { todo -> + val todoDetails = todo.toTodoDetails() + navController.navigate(todoDetails) } ) } - composable { entry -> - val bookDetails = entry.toRoute() - val book = bookDetails.toBook() - BookDetailsScreen( - book = book, + composable { + ProfileScreen(userViewModel) + } + composable { + HomeScreen(walletViewModel) + } + composable { + LoginScreen(loginViewModel, onLoginSuccess = { token -> + navController.navigate(UserScreen) + }) + } + + composable { + CreateUserScreen(createUserViewModel, onCreateUserSuccess = { user -> + navController.navigate(UserScreen) + }) + } + composable { entry -> + val todoDetails = entry.toRoute() + val todo = todoDetails.toTodo() + TodoDetailsScreen ( + todo = todo, navigateBack = navController::navigateUp ) } diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/navigation/Route.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/navigation/Route.kt index 47a2301..0b1281a 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/navigation/Route.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/navigation/Route.kt @@ -1,20 +1,33 @@ package ro.alexmamo.roomjetpackcompose.navigation import kotlinx.serialization.Serializable -import ro.alexmamo.roomjetpackcompose.domain.model.Book +import ro.alexmamo.roomjetpackcompose.domain.model.Todo + + +@Serializable +object TodoListScreen + +@Serializable +object LoginScreen + +@Serializable +object CreateUserScreen + +@Serializable +object HomeScreen @Serializable -object BookListScreen +object UserScreen @Serializable -data class BookDetails( +data class TodoDetails( val id: Int, - val title: String, - val author: String + val name: String, + val description: String ) -fun BookDetails.toBook() = Book( +fun TodoDetails.toTodo() = Todo( id = this.id, - title = this.title, - author = this.author + name = this.name, + description = this.description ) \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/MainActivity.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/MainActivity.kt index b4623be..b94568a 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/MainActivity.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/MainActivity.kt @@ -6,15 +6,18 @@ import androidx.activity.compose.setContent import androidx.navigation.compose.rememberNavController import dagger.hilt.android.AndroidEntryPoint import ro.alexmamo.roomjetpackcompose.navigation.NavGraph +import ro.alexmamo.roomjetpackcompose.ui.theme.CustomTheme @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - NavGraph( - navController = rememberNavController() - ) + CustomTheme { + NavGraph( + navController = rememberNavController() + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/BookDetailsScreen.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/BookDetailsScreen.kt deleted file mode 100644 index c3920c4..0000000 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/BookDetailsScreen.kt +++ /dev/null @@ -1,27 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_details - -import androidx.compose.material.Scaffold -import androidx.compose.runtime.Composable -import ro.alexmamo.roomjetpackcompose.domain.model.Book -import ro.alexmamo.roomjetpackcompose.presentation.book_details.components.BookDetailsContent -import ro.alexmamo.roomjetpackcompose.presentation.book_details.components.BookDetailsTopBar - -@Composable -fun BookDetailsScreen( - book: Book, - navigateBack: () -> Unit -) { - Scaffold( - topBar = { - BookDetailsTopBar( - onArrowBackIconClick = navigateBack - ) - }, - content = { innerPadding -> - BookDetailsContent( - innerPadding = innerPadding, - book = book - ) - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListViewModel.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListViewModel.kt deleted file mode 100644 index f9e932c..0000000 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListViewModel.kt +++ /dev/null @@ -1,85 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import ro.alexmamo.roomjetpackcompose.domain.model.Book -import ro.alexmamo.roomjetpackcompose.domain.model.Response -import ro.alexmamo.roomjetpackcompose.domain.repository.BookRepository -import javax.inject.Inject - -typealias InsertBookResponse = Response -typealias UpdateBookResponse = Response -typealias DeleteBookResponse = Response - -@HiltViewModel -class BookListViewModel @Inject constructor( - private val repo: BookRepository -) : ViewModel() { - val bookListState = repo.getBookList().map { bookList -> - try { - Response.Success(bookList) - } catch (e: Exception) { - Response.Failure(e) - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = Response.Loading - ) - - private val _insertBookState = MutableStateFlow(Response.Idle) - val insertBookState: StateFlow = _insertBookState.asStateFlow() - - private val _updateBookState = MutableStateFlow(Response.Idle) - val updateBookState: StateFlow = _updateBookState.asStateFlow() - - private val _deleteBookState = MutableStateFlow(Response.Idle) - val deleteBookState: StateFlow = _deleteBookState.asStateFlow() - - fun insertBook(book: Book) = viewModelScope.launch { - try { - _insertBookState.value = Response.Loading - _insertBookState.value = Response.Success(repo.insertBook(book)) - } catch (e: Exception) { - Response.Failure(e) - } - } - - fun resetInsertBookState() { - _insertBookState.value = Response.Idle - } - - fun updateBook(book: Book) = viewModelScope.launch { - try { - _updateBookState.value = Response.Loading - _updateBookState.value = Response.Success(repo.updateBook(book)) - } catch (e: Exception) { - Response.Failure(e) - } - } - - fun resetUpdateBookState() { - _updateBookState.value = Response.Idle - } - - fun deleteBook(book: Book) = viewModelScope.launch { - try { - _deleteBookState.value = Response.Loading - _deleteBookState.value = Response.Success(repo.deleteBook(book)) - } catch (e: Exception) { - Response.Failure(e) - } - } - - fun resetDeleteBookState() { - _deleteBookState.value = Response.Idle - } -} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListContent.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListContent.kt deleted file mode 100644 index 5bc43a0..0000000 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListContent.kt +++ /dev/null @@ -1,72 +0,0 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import ro.alexmamo.roomjetpackcompose.domain.model.Book - -const val NON_EXISTENT_BOOK_ID = -1 - -@Composable -fun BookListContent( - innerPadding: PaddingValues, - bookList: List, - onBookCardClick: (Book) -> Unit, - onUpdateBook: (Book) -> Unit, - onEmptyBookField: (String) -> Unit, - onDeleteBook: (Book) -> Unit, - onNoBookUpdates: () -> Unit -) { - var editBookId by remember { mutableIntStateOf(NON_EXISTENT_BOOK_ID) } - - LazyColumn( - modifier = Modifier.fillMaxSize().padding(innerPadding) - ) { - items( - items = bookList, - key = { book -> - book.id - } - ) { book -> - if (editBookId != book.id) { - BookCard( - book = book, - onBookCardClick = { - onBookCardClick(book) - }, - onEditBook = { - editBookId = book.id - }, - onDeleteBook = { - onDeleteBook(book) - editBookId = NON_EXISTENT_BOOK_ID - } - ) - } else { - EditableBookCard( - book = book, - onUpdateBook = { updatedBook -> - onUpdateBook(updatedBook) - editBookId = NON_EXISTENT_BOOK_ID - }, - onEmptyBookField = onEmptyBookField, - onNoBookUpdates = { - onNoBookUpdates() - editBookId = NON_EXISTENT_BOOK_ID - }, - onCancel = { - editBookId = NON_EXISTENT_BOOK_ID - } - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/create_user/CreateUserScreen.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/create_user/CreateUserScreen.kt new file mode 100644 index 0000000..82efc68 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/create_user/CreateUserScreen.kt @@ -0,0 +1,34 @@ +package ro.alexmamo.roomjetpackcompose.presentation.create_user + +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.lifecycle.viewmodel.compose.viewModel +import ro.alexmamo.roomjetpackcompose.infraestructure.user.User + +@Composable +fun CreateUserScreen( + viewModel: CreateUserViewModel = viewModel(), + onCreateUserSuccess: (User) -> Unit +) { + val uiState by viewModel.uiState.collectAsState() + + Button(onClick = { viewModel.createUser("email", "pass", "user") }) { + Text("Crear usuario") + } + + when (uiState) { + is CreateUserViewModel.UiState.Loading -> Text("Cargando...") + is CreateUserViewModel.UiState.Success -> { + val user = (uiState as CreateUserViewModel.UiState.Success).user + LaunchedEffect(Unit) { + onCreateUserSuccess(user) + } + } + is CreateUserViewModel.UiState.Error -> Text("Error: ${(uiState as CreateUserViewModel.UiState.Error).message}") + else -> {} + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/create_user/CreateUserViewModel.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/create_user/CreateUserViewModel.kt new file mode 100644 index 0000000..87a9177 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/create_user/CreateUserViewModel.kt @@ -0,0 +1,38 @@ +package ro.alexmamo.roomjetpackcompose.presentation.create_user + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import ro.alexmamo.roomjetpackcompose.infraestructure.auth.Auth +import ro.alexmamo.roomjetpackcompose.infraestructure.auth.AuthImpl +import ro.alexmamo.roomjetpackcompose.infraestructure.auth.CreateUserRequest +import ro.alexmamo.roomjetpackcompose.infraestructure.auth.Token +import ro.alexmamo.roomjetpackcompose.infraestructure.user.User + + +class CreateUserViewModel(private val auth: Auth = AuthImpl()) : ViewModel() { + + sealed class UiState { + object Idle : UiState() + object Loading : UiState() + data class Success(val user: User) : UiState() + data class Error(val message: String) : UiState() + } + + private val _uiState = MutableStateFlow(UiState.Idle) + val uiState = _uiState.asStateFlow() + + fun createUser(email: String, password: String, username: String) { + viewModelScope.launch { + _uiState.value = UiState.Loading + val user = auth.createUser(CreateUserRequest(email, password, username)) + if (user != null) { + _uiState.value = UiState.Success(user) + } else { + _uiState.value = UiState.Error("Error al crear usuario") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/home/HomeScreen.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/home/HomeScreen.kt new file mode 100644 index 0000000..904de17 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/home/HomeScreen.kt @@ -0,0 +1,90 @@ +package ro.alexmamo.roomjetpackcompose.presentation.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import ro.alexmamo.roomjetpackcompose.infraestructure.wallet.Transaction + +@Composable +fun HomeScreen(viewModel: WalletViewModel = viewModel()) { + val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(Unit) { + viewModel.get() + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + ) { + when (uiState) { + is WalletViewModel.UiState.Idle -> Text("Esperando acción…") + + is WalletViewModel.UiState.Loading -> Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + + is WalletViewModel.UiState.Success -> { + val wallet = (uiState as WalletViewModel.UiState.Success).wallet + + Text(text = "💰 Balance: ${wallet.balance}") + Text(text = "Ingresos: ${wallet.income}") + Text(text = "Gastos: ${wallet.expense}") + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Transacciones" + ) + + Spacer(modifier = Modifier.height(8.dp)) + + LazyColumn { + items(wallet.transactions) { tx -> + TransactionRow(tx) + } + } + } + + is WalletViewModel.UiState.Error -> Text( + text = "Error: ${(uiState as WalletViewModel.UiState.Error).message}" + ) + } + } +} + +@Composable +fun TransactionRow(tx: Transaction) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) { + Text("📄 ${tx.description}") + Text("Fecha: ${tx.date}") + Text("Monto: ${tx.amount} ${tx.currency}") + Text("Tipo: ${tx.type} - ${tx.subtype}") + Divider(modifier = Modifier.padding(top = 8.dp)) + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/home/WalletViewModel.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/home/WalletViewModel.kt new file mode 100644 index 0000000..b4245f3 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/home/WalletViewModel.kt @@ -0,0 +1,35 @@ +package ro.alexmamo.roomjetpackcompose.presentation.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import ro.alexmamo.roomjetpackcompose.infraestructure.wallet.Wallet +import ro.alexmamo.roomjetpackcompose.infraestructure.wallet.WalletImpl +import ro.alexmamo.roomjetpackcompose.infraestructure.wallet.WalletInterface + +class WalletViewModel(private val walletInterface: WalletInterface = WalletImpl()) : ViewModel() { + + sealed class UiState { + object Idle : UiState() + object Loading : UiState() + data class Success(val wallet: Wallet) : UiState() + data class Error(val message: String) : UiState() + } + + private val _uiState = MutableStateFlow(UiState.Idle) + val uiState = _uiState.asStateFlow() + + fun get() { + viewModelScope.launch { + _uiState.value = UiState.Loading + val wallet = walletInterface.get() + if (wallet != null) { + _uiState.value = UiState.Success(wallet) + } else { + _uiState.value = UiState.Error("Error al traer transacciones") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/layouts/BaseScreen.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/layouts/BaseScreen.kt new file mode 100644 index 0000000..8565cbb --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/layouts/BaseScreen.kt @@ -0,0 +1,109 @@ +package ro.alexmamo.roomjetpackcompose.presentation.layouts + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.FabPosition +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import ro.alexmamo.roomjetpackcompose.components.AppTopBar +import ro.alexmamo.roomjetpackcompose.ui.theme.Dimens + +/** + * BaseScreen:`. + * - soporta variante centrada o normal + * - slots: topBar, header, content, bottomBar, fab + * - mantiene paddings y radios desde `ui.theme.Dimens` + */ + +@Composable +fun BaseScreen( + modifier: Modifier = Modifier, + title: String? = null, + centerContent: Boolean = false, + topBar: (@Composable () -> Unit)? = null, + header: (@Composable () -> Unit)? = null, + content: @Composable (paddingValues: PaddingValues) -> Unit, + bottomBar: (@Composable () -> Unit)? = null, + fab: (@Composable () -> Unit)? = null, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() } +) { + Scaffold( + modifier = modifier, + topBar = { + // sirve para qe la camara no recorte el título del top bar. + Box(modifier = Modifier.statusBarsPadding()) { + when { + topBar != null -> topBar() + title != null -> AppTopBar(title = title) + } + } + }, + floatingActionButton = { if (fab != null) fab() }, + floatingActionButtonPosition = FabPosition.End, + bottomBar = { if (bottomBar != null) bottomBar() }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) } + ) { paddingValues -> + val onlyTopPadding = PaddingValues(top = paddingValues.calculateTopPadding()) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(onlyTopPadding) + ) { + if (header != null) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = Dimens.paddingLarge, + vertical = Dimens.paddingSmall + ) + ) { + header() + } + } + + // contenid prinicipal + Box( + modifier = Modifier + .fillMaxSize() + .clip( + RoundedCornerShape( + topStart = 56.dp, + topEnd = 56.dp + ) + ) + .background(MaterialTheme.colorScheme.surface) + .padding(Dimens.paddingLarge) + ) { + if (centerContent) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + content(PaddingValues(0.dp)) + } + } else { + content(onlyTopPadding) + } + } + } + } +} diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/login/LoginScreen.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/login/LoginScreen.kt new file mode 100644 index 0000000..8033fa8 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/login/LoginScreen.kt @@ -0,0 +1,34 @@ +package ro.alexmamo.roomjetpackcompose.presentation.login + +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.lifecycle.viewmodel.compose.viewModel +import ro.alexmamo.roomjetpackcompose.infraestructure.auth.Token + +@Composable +fun LoginScreen( + viewModel: LoginViewModel = viewModel(), + onLoginSuccess: (Token) -> Unit +) { + val uiState by viewModel.uiState.collectAsState() + + Button(onClick = { viewModel.login("user", "pass") }) { + Text("Iniciar sesión") + } + + when (uiState) { + is LoginViewModel.UiState.Loading -> Text("Cargando...") + is LoginViewModel.UiState.Success -> { + val token = (uiState as LoginViewModel.UiState.Success).token + LaunchedEffect(Unit) { + onLoginSuccess(token) // redirigís acá + } + } + is LoginViewModel.UiState.Error -> Text("Error: ${(uiState as LoginViewModel.UiState.Error).message}") + else -> {} + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/login/LoginViewModel.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/login/LoginViewModel.kt new file mode 100644 index 0000000..e7aad8e --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/login/LoginViewModel.kt @@ -0,0 +1,36 @@ +package ro.alexmamo.roomjetpackcompose.presentation.login + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import ro.alexmamo.roomjetpackcompose.infraestructure.auth.Auth +import ro.alexmamo.roomjetpackcompose.infraestructure.auth.AuthImpl +import ro.alexmamo.roomjetpackcompose.infraestructure.auth.LoginRequest +import ro.alexmamo.roomjetpackcompose.infraestructure.auth.Token + +class LoginViewModel(private val auth: Auth = AuthImpl()) : ViewModel() { + + sealed class UiState { + object Idle : UiState() + object Loading : UiState() + data class Success(val token: Token) : UiState() + data class Error(val message: String) : UiState() + } + + private val _uiState = MutableStateFlow(UiState.Idle) + val uiState = _uiState.asStateFlow() + + fun login(username: String, password: String) { + viewModelScope.launch { + _uiState.value = UiState.Loading + val token = auth.login(LoginRequest(username, password)) + if (token != null) { + _uiState.value = UiState.Success(token) + } else { + _uiState.value = UiState.Error("Error al iniciar sesión") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/notification/NotificationScreen.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/notification/NotificationScreen.kt new file mode 100644 index 0000000..d682581 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/notification/NotificationScreen.kt @@ -0,0 +1,111 @@ +package ro.alexmamo.roomjetpackcompose.presentation.notification + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import ro.alexmamo.roomjetpackcompose.R +import ro.alexmamo.roomjetpackcompose.components.ActionIconButton +import ro.alexmamo.roomjetpackcompose.components.AppTopBar +import ro.alexmamo.roomjetpackcompose.components.BottomNavigationBar +import ro.alexmamo.roomjetpackcompose.components.NotificationItem +import ro.alexmamo.roomjetpackcompose.presentation.layouts.BaseScreen +import ro.alexmamo.roomjetpackcompose.ui.theme.Dimens +import ro.alexmamo.roomjetpackcompose.ui.theme.Honeydew + +@Composable +fun NotificationScreen(vm: NotificationViewModel = viewModel()) { + BaseScreen( + title = null, + topBar = { + AppTopBar( + title = stringResource(R.string.notification_title), + leftAction = { + ActionIconButton( + onActionIconButtonClick = { /* para atras */ }, + withCircle = false, + content = { mod -> + androidx.compose.material.Icon( + painter = painterResource(id = R.drawable.arrow_left), + contentDescription = stringResource(id = R.string.navigate_back), + tint = Honeydew, + modifier = mod + ) + } + ) + }, + rightAction = { + ActionIconButton( + onActionIconButtonClick = { /* esto la verdad nose qe haria porque ya estamos en notif */ }, + withCircle = true, + circleSize = 30.dp, + circleColor = MaterialTheme.colorScheme.surface, + content = { mod -> + androidx.compose.material.Icon( + painter = painterResource(id = R.drawable.notification), + contentDescription = stringResource(id = R.string.notification_title), + tint = MaterialTheme.colorScheme.onSecondary, + modifier = mod + ) + } + ) + } + ) + }, + bottomBar = { BottomNavigationBar() }, + content = { _ -> + val sections by vm.sections.collectAsState() + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(Dimens.paddingMedium), + verticalArrangement = Arrangement.spacedBy(Dimens.paddingMedium), + contentPadding = PaddingValues(bottom = Dimens.paddingLarge) + ) { + sections.forEach { section -> + item(key = section.titleRes) { + val sectionTitle = stringResource(id = section.titleRes) + Text( + sectionTitle, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSecondary + ) + } + items(section.items) { item -> + NotificationItem( + iconRes = item.iconRes, + titleRes = item.titleRes, + messageRes = item.messageRes, + timeRes = item.timeRes + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Dimens.paddingMedium) + .height(2.dp) + .background(color = MaterialTheme.colorScheme.background) + ) + } + } + } + } + ) +} diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/notification/NotificationViewModel.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/notification/NotificationViewModel.kt new file mode 100644 index 0000000..487594a --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/notification/NotificationViewModel.kt @@ -0,0 +1,49 @@ +package ro.alexmamo.roomjetpackcompose.presentation.notification + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import ro.alexmamo.roomjetpackcompose.R + +class NotificationViewModel : ViewModel() { + + data class Item( + val iconRes: Int, + val titleRes: Int, + val messageRes: Int, + val timeRes: Int + ) + + data class Section( + val titleRes: Int, + val items: List + ) + + private val _sections = MutableStateFlow( + listOf( + Section( + titleRes = R.string.today_section_title, + items = listOf( + Item(R.drawable.notification, R.string.notif_reminder_title, R.string.notif_reminder_message, R.string.notification_time_example), + Item(R.drawable.star, R.string.notif_new_update_title, R.string.notif_reminder_message, R.string.notification_time_example), + ) + ), + Section( + titleRes = R.string.yesterday_section_title, + items = listOf( + Item(R.drawable.dollar, R.string.notif_transactions_title, R.string.notif_transactions_message, R.string.notification_time_example), + Item(R.drawable.notification, R.string.notif_reminder_title, R.string.notif_reminder_message, R.string.notification_time_example), + ) + ), + Section( + titleRes = R.string.thisweekend_section_title, + items = listOf( + Item(R.drawable.arrow_down, R.string.notif_expense_title, R.string.notif_expense_message, R.string.notification_time_example), + Item(R.drawable.transactions, R.string.notif_transactions_title, R.string.notif_transactions_message, R.string.notification_time_example), + ) + ), + ) + ) + + val sections: StateFlow> = _sections +} diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/profile/ProfileScreen.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/profile/ProfileScreen.kt new file mode 100644 index 0000000..b93c45d --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/profile/ProfileScreen.kt @@ -0,0 +1,50 @@ +package ro.alexmamo.roomjetpackcompose.presentation.profile + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel + +@Composable +fun ProfileScreen(viewModel: UserViewModel = viewModel()) { + val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(Unit) { + viewModel.fetchUser(1) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + when (uiState) { + is UserViewModel.UiState.Idle -> Text("Esperando acción…") + is UserViewModel.UiState.Loading -> CircularProgressIndicator() + is UserViewModel.UiState.Success -> { + val user = (uiState as UserViewModel.UiState.Success).user + Text("Usuario: ${user.name.firstname} ${user.name.lastname}") + Spacer(Modifier.height(8.dp)) + Text("Email: ${user.email}") + Text("Ciudad: ${user.address.city}") + } + is UserViewModel.UiState.Error -> Text( + "Error: ${(uiState as UserViewModel.UiState.Error).message}", + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/profile/UserViewModel.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/profile/UserViewModel.kt new file mode 100644 index 0000000..a9bb2e8 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/profile/UserViewModel.kt @@ -0,0 +1,40 @@ +package ro.alexmamo.roomjetpackcompose.presentation.profile + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +import ro.alexmamo.roomjetpackcompose.infraestructure.user.User +import ro.alexmamo.roomjetpackcompose.infraestructure.user.Users +import ro.alexmamo.roomjetpackcompose.infraestructure.user.UsersImpl + +class UserViewModel( + private val users: Users = UsersImpl() +) : ViewModel() { + + sealed class UiState { + object Idle : UiState() + object Loading : UiState() + data class Success(val user: User) : UiState() + data class Error(val message: String) : UiState() + } + + private val _uiState = MutableStateFlow(UserViewModel.UiState.Idle) + val uiState = _uiState.asStateFlow() + + fun fetchUser(id: Int) { + viewModelScope.launch { + _uiState.value = UserViewModel.UiState.Loading + val user = users.getById(id) + if (user != null) { + _uiState.value = UserViewModel.UiState.Success(user) + } else { + _uiState.value = UserViewModel.UiState.Error("Error al traer usuario") + } + } + } +} + +private fun Any.launch(function: () -> Unit) {} diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_details/TodoDetailsScreen.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_details/TodoDetailsScreen.kt new file mode 100644 index 0000000..3a12231 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_details/TodoDetailsScreen.kt @@ -0,0 +1,27 @@ +package ro.alexmamo.roomjetpackcompose.presentation.todo_details + +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import ro.alexmamo.roomjetpackcompose.domain.model.Todo +import ro.alexmamo.roomjetpackcompose.presentation.todo_details.components.TodoDetailsContent +import ro.alexmamo.roomjetpackcompose.presentation.todo_details.components.TodoDetailsTopBar + +@Composable +fun TodoDetailsScreen( + todo: Todo, + navigateBack: () -> Unit +) { + Scaffold( + topBar = { + TodoDetailsTopBar( + onArrowBackIconClick = navigateBack + ) + }, + content = { innerPadding -> + TodoDetailsContent( + innerPadding = innerPadding, + todo = todo + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/components/BookDetailsContent.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_details/components/TodoDetailsContent.kt similarity index 53% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/components/BookDetailsContent.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_details/components/TodoDetailsContent.kt index 2196534..bd60638 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/components/BookDetailsContent.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_details/components/TodoDetailsContent.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_details.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_details.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -7,23 +7,23 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import ro.alexmamo.roomjetpackcompose.domain.model.Book -import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.AuthorText -import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.TitleText +import ro.alexmamo.roomjetpackcompose.domain.model.Todo +import ro.alexmamo.roomjetpackcompose.presentation.todo_list.components.DescriptionText +import ro.alexmamo.roomjetpackcompose.presentation.todo_list.components.NameText @Composable -fun BookDetailsContent( +fun TodoDetailsContent( innerPadding: PaddingValues, - book: Book + todo: Todo ) { Column( modifier = Modifier.fillMaxSize().padding(innerPadding).padding(8.dp) ) { - TitleText( - title = book.title + NameText( + title = todo.name ) - AuthorText( - author = book.author + DescriptionText( + author = todo.description ) } } \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/components/BookDetailsTopBar.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_details/components/TodoDetailsTopBar.kt similarity index 51% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/components/BookDetailsTopBar.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_details/components/TodoDetailsTopBar.kt index 556af02..570774c 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_details/components/BookDetailsTopBar.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_details/components/TodoDetailsTopBar.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_details.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_details.components import androidx.compose.material.Text import androidx.compose.material.TopAppBar @@ -7,25 +7,36 @@ import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import ro.alexmamo.roomjetpackcompose.R +import androidx.compose.material3.Icon +import androidx.compose.ui.unit.dp +import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.layout.size import ro.alexmamo.roomjetpackcompose.components.ActionIconButton @Composable -fun BookDetailsTopBar( +fun TodoDetailsTopBar( onArrowBackIconClick: () -> Unit ) { TopAppBar ( title = { Text( text = stringResource( - id = R.string.book_details_screen_title + id = R.string.todo_details_screen_name ) ) }, navigationIcon = { ActionIconButton( onActionIconButtonClick = onArrowBackIconClick, - imageVector = Icons.AutoMirrored.Outlined.ArrowBack, - resourceId = R.string.navigate_back + withCircle = false, + content = { mod -> + Icon( + Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = stringResource(id = R.string.navigate_back), + tint = MaterialTheme.colorScheme.onSurface, + modifier = mod + ) + } ) } ) diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListScreen.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/TodoListScreen.kt similarity index 58% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListScreen.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/TodoListScreen.kt index 49b8a0f..bc8772d 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/BookListScreen.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/TodoListScreen.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list +package ro.alexmamo.roomjetpackcompose.presentation.todo_list import androidx.compose.material.Scaffold import androidx.compose.material.SnackbarHost @@ -13,42 +13,43 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage import ro.alexmamo.roomjetpackcompose.R import ro.alexmamo.roomjetpackcompose.components.LoadingIndicator import ro.alexmamo.roomjetpackcompose.core.logMessage import ro.alexmamo.roomjetpackcompose.core.showSnackbarMessage import ro.alexmamo.roomjetpackcompose.core.showToastMessage -import ro.alexmamo.roomjetpackcompose.domain.model.Book +import ro.alexmamo.roomjetpackcompose.domain.model.Todo import ro.alexmamo.roomjetpackcompose.domain.model.Response -import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.BookListContent -import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.BookListTopBar -import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.EmptyBookListContent -import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.InsertBookAlertDialog -import ro.alexmamo.roomjetpackcompose.presentation.book_list.components.InsertBookFloatingActionButton +import ro.alexmamo.roomjetpackcompose.presentation.todo_list.components.TodoListContent +import ro.alexmamo.roomjetpackcompose.presentation.todo_list.components.TodoListTopBar +import ro.alexmamo.roomjetpackcompose.presentation.todo_list.components.EmptyTodoListContent +import ro.alexmamo.roomjetpackcompose.presentation.todo_list.components.InsertTodoAlertDialog +import ro.alexmamo.roomjetpackcompose.presentation.todo_list.components.InsertFloatingActionButton @Composable -fun BookListScreen( - viewModel: BookListViewModel = hiltViewModel(), - navigateToBookDetailsScreen: (Book) -> Unit +fun TodoListScreen( + viewModel: TodoListViewModel = hiltViewModel(), + navigateToTodoDetailsScreen: (Todo) -> Unit ) { val context = LocalContext.current val resources = context.resources val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - var openInsertBookDialog by remember { mutableStateOf(false) } - val bookListResponse by viewModel.bookListState.collectAsStateWithLifecycle() - val insertBookResponse by viewModel.insertBookState.collectAsStateWithLifecycle() - val updateBookResponse by viewModel.updateBookState.collectAsStateWithLifecycle() - val deleteBookResponse by viewModel.deleteBookState.collectAsStateWithLifecycle() + var openInsertTodoDialog by remember { mutableStateOf(false) } + val todoListResponse by viewModel.todoListState.collectAsStateWithLifecycle() + val insertTodoResponse by viewModel.insertTodoState.collectAsStateWithLifecycle() + val updateTodoResponse by viewModel.updateTodoState.collectAsStateWithLifecycle() + val deleteTodoResponse by viewModel.deleteTodoState.collectAsStateWithLifecycle() Scaffold( topBar = { - BookListTopBar() + TodoListTopBar() }, floatingActionButton = { - InsertBookFloatingActionButton( - onInsertBookFloatingActionButtonClick = { - openInsertBookDialog = true + InsertFloatingActionButton( + onInsertFloatingActionButtonClick = { + openInsertTodoDialog = true } ) }, @@ -58,43 +59,43 @@ fun BookListScreen( ) } ) { innerPadding -> - when(val bookListResponse = bookListResponse) { + when(val todoListResponse = todoListResponse) { is Response.Idle -> {} is Response.Loading -> LoadingIndicator() - is Response.Success -> bookListResponse.data.let { bookList -> - if (bookList.isEmpty()) { - EmptyBookListContent( + is Response.Success -> todoListResponse.data.let { todoList -> + if (todoList.isEmpty()) { + EmptyTodoListContent( innerPadding = innerPadding ) } else { - BookListContent( + TodoListContent( innerPadding = innerPadding, - bookList = bookList, - onBookCardClick = navigateToBookDetailsScreen, - onUpdateBook = { book -> - viewModel.updateBook(book) + todoList = todoList, + onTodoCardClick = navigateToTodoDetailsScreen, + onUpdateTodo = { todo -> + viewModel.updateTodo(todo) }, - onEmptyBookField = { bookField -> + onEmptyTodoField = { todoField -> showSnackbarMessage( coroutineScope = coroutineScope, snackbarHostState = snackbarHostState, - message = resources.getString(R.string.empty_book_field_message, bookField) + message = resources.getString(R.string.empty_todo_field_message, todoField) ) }, - onDeleteBook = { bookId -> - viewModel.deleteBook(bookId) + onDeleteTodo = { todoId -> + viewModel.deleteTodo(todoId) }, - onNoBookUpdates = { + onNoTodoUpdates = { showSnackbarMessage( coroutineScope = coroutineScope, snackbarHostState = snackbarHostState, - message = resources.getString(R.string.no_book_updates_message) + message = resources.getString(R.string.no_todo_updates_message) ) } ) } } - is Response.Failure -> bookListResponse.e.message?.let { errorMessage -> + is Response.Failure -> todoListResponse.e.message?.let { errorMessage -> LaunchedEffect(errorMessage) { logMessage(errorMessage) showToastMessage(context, errorMessage) @@ -103,36 +104,36 @@ fun BookListScreen( } } - if (openInsertBookDialog) { - InsertBookAlertDialog( - onInsertBook = { book -> - viewModel.insertBook(book) + if (openInsertTodoDialog) { + InsertTodoAlertDialog( + onInsertTodo = { todo -> + viewModel.insertTodo(todo) }, - onEmptyBookField = { emptyField -> + onEmptyTodoField = { emptyField -> showSnackbarMessage( coroutineScope = coroutineScope, snackbarHostState = snackbarHostState, - message = resources.getString(R.string.empty_book_field_message, emptyField) + message = resources.getString(R.string.empty_todo_field_message, emptyField) ) }, - onInsertBookDialogCancel = { - openInsertBookDialog = false + onInsertTodoDialogCancel = { + openInsertTodoDialog = false } ) } - when(val insertBookResponse = insertBookResponse) { + when(val insertTodoResponse = insertTodoResponse) { is Response.Idle -> {} is Response.Loading -> LoadingIndicator() is Response.Success -> LaunchedEffect(Unit) { showSnackbarMessage( coroutineScope = coroutineScope, snackbarHostState = snackbarHostState, - message = resources.getString(R.string.book_action_message, BookAction.ADDED) + message = resources.getString(R.string.todo_action_message, TodoAction.ADDED) ) - viewModel.resetInsertBookState() + viewModel.resetInsertTodoState() } - is Response.Failure -> insertBookResponse.e.message?.let { errorMessage -> + is Response.Failure -> insertTodoResponse.e.message?.let { errorMessage -> LaunchedEffect(errorMessage) { logMessage(errorMessage) showToastMessage(context, errorMessage) @@ -140,18 +141,18 @@ fun BookListScreen( } } - when(val updateBookResponse = updateBookResponse) { + when(val updateTodoResponse = updateTodoResponse) { is Response.Idle -> {} is Response.Loading -> LoadingIndicator() is Response.Success -> LaunchedEffect(Unit) { showSnackbarMessage( coroutineScope = coroutineScope, snackbarHostState = snackbarHostState, - message = resources.getString(R.string.book_action_message, BookAction.UPDATED) + message = resources.getString(R.string.todo_action_message, TodoAction.UPDATED) ) - viewModel.resetUpdateBookState() + viewModel.resetUpdateTodoState() } - is Response.Failure -> updateBookResponse.e.message?.let { errorMessage -> + is Response.Failure -> updateTodoResponse.e.message?.let { errorMessage -> LaunchedEffect(errorMessage) { logMessage(errorMessage) showToastMessage(context, errorMessage) @@ -159,18 +160,18 @@ fun BookListScreen( } } - when(val deleteBookResponse = deleteBookResponse) { + when(val deleteTodoResponse = deleteTodoResponse) { is Response.Idle -> {} is Response.Loading -> LoadingIndicator() is Response.Success -> LaunchedEffect(Unit) { showSnackbarMessage( coroutineScope = coroutineScope, snackbarHostState = snackbarHostState, - message = resources.getString(R.string.book_action_message, BookAction.DELETED) + message = resources.getString(R.string.todo_action_message, TodoAction.DELETED) ) - viewModel.resetDeleteBookState() + viewModel.resetDeleteTodoState() } - is Response.Failure -> deleteBookResponse.e.message?.let { errorMessage -> + is Response.Failure -> deleteTodoResponse.e.message?.let { errorMessage -> LaunchedEffect(errorMessage) { logMessage(errorMessage) showToastMessage(context, errorMessage) @@ -179,7 +180,7 @@ fun BookListScreen( } } -enum class BookAction() { +enum class TodoAction() { ADDED, UPDATED, DELETED diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/TodoListViewModel.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/TodoListViewModel.kt new file mode 100644 index 0000000..35bceea --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/TodoListViewModel.kt @@ -0,0 +1,85 @@ +package ro.alexmamo.roomjetpackcompose.presentation.todo_list + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import ro.alexmamo.roomjetpackcompose.domain.model.Todo +import ro.alexmamo.roomjetpackcompose.domain.model.Response +import ro.alexmamo.roomjetpackcompose.domain.repository.TodoRepository +import javax.inject.Inject + +typealias InsertTodoResponse = Response +typealias UpdateTodoResponse = Response +typealias DeleteTodoResponse = Response + +@HiltViewModel +class TodoListViewModel @Inject constructor( + private val repo: TodoRepository +) : ViewModel() { + val todoListState = repo.getTodoList().map { todoList -> + try { + Response.Success(todoList) + } catch (e: Exception) { + Response.Failure(e) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = Response.Loading + ) + + private val _insertTodoState = MutableStateFlow(Response.Idle) + val insertTodoState: StateFlow = _insertTodoState.asStateFlow() + + private val _updateTodoState = MutableStateFlow(Response.Idle) + val updateTodoState: StateFlow = _updateTodoState.asStateFlow() + + private val _deleteTodoState = MutableStateFlow(Response.Idle) + val deleteTodoState: StateFlow = _deleteTodoState.asStateFlow() + + fun insertTodo(todo: Todo) = viewModelScope.launch { + try { + _insertTodoState.value = Response.Loading + _insertTodoState.value = Response.Success(repo.insertTodo(todo)) + } catch (e: Exception) { + Response.Failure(e) + } + } + + fun resetInsertTodoState() { + _insertTodoState.value = Response.Idle + } + + fun updateTodo(todo: Todo) = viewModelScope.launch { + try { + _updateTodoState.value = Response.Loading + _updateTodoState.value = Response.Success(repo.updateTodo(todo)) + } catch (e: Exception) { + Response.Failure(e) + } + } + + fun resetUpdateTodoState() { + _updateTodoState.value = Response.Idle + } + + fun deleteTodo(todo: Todo) = viewModelScope.launch { + try { + _deleteTodoState.value = Response.Loading + _deleteTodoState.value = Response.Success(repo.deleteTodo(todo)) + } catch (e: Exception) { + Response.Failure(e) + } + } + + fun resetDeleteTodoState() { + _deleteTodoState.value = Response.Idle + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/AuthorText.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/DescriptionText.kt similarity index 82% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/AuthorText.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/DescriptionText.kt index df685a3..9bd3a31 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/AuthorText.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/DescriptionText.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_list.components import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -7,7 +7,7 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.sp @Composable -fun AuthorText( +fun DescriptionText( author: String ) { Text( diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/AuthorTextField.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/DescriptionTextField.kt similarity index 56% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/AuthorTextField.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/DescriptionTextField.kt index daff6db..7423424 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/AuthorTextField.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/DescriptionTextField.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_list.components import androidx.compose.material.Text import androidx.compose.material.TextField @@ -11,22 +11,22 @@ import androidx.compose.ui.res.stringResource import ro.alexmamo.roomjetpackcompose.R @Composable -fun AuthorTextField( - author: String, - onUpdateAuthor: (String) -> Unit +fun DescriptionTextField( + description: String, + onUpdateDescription: (String) -> Unit ) { - var author by remember { mutableStateOf(author) } + var description by remember { mutableStateOf(description) } TextField( - value = author, - onValueChange = { newAuthor -> - author = newAuthor - onUpdateAuthor(newAuthor) + value = description, + onValueChange = { newDescription -> + description = newDescription + onUpdateDescription(newDescription) }, placeholder = { Text( text = stringResource( - id = R.string.book_author + id = R.string.todo_description ) ) } diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/EditableBookCard.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/EditableTodoCard.kt similarity index 59% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/EditableBookCard.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/EditableTodoCard.kt index 59f8737..6ee71e7 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/EditableBookCard.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/EditableTodoCard.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_list.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -18,19 +18,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ro.alexmamo.roomjetpackcompose.R import ro.alexmamo.roomjetpackcompose.components.ActionButton -import ro.alexmamo.roomjetpackcompose.core.AUTHOR_FIELD -import ro.alexmamo.roomjetpackcompose.core.TITLE_FIELD -import ro.alexmamo.roomjetpackcompose.domain.model.Book +import ro.alexmamo.roomjetpackcompose.core.DESCRIPTION_FIELD +import ro.alexmamo.roomjetpackcompose.core.NAME_FIELD +import ro.alexmamo.roomjetpackcompose.domain.model.Todo @Composable -fun EditableBookCard( - book: Book, - onUpdateBook: (Book) -> Unit, - onEmptyBookField: (String) -> Unit, - onNoBookUpdates: () -> Unit, +fun EditableTodoCard( + todo: Todo, + onUpdateTodo: (Todo) -> Unit, + onEmptyTodoField: (String) -> Unit, + onNoTodoUpdates: () -> Unit, onCancel: () -> Unit ) { - var updatedBook by remember { mutableStateOf(book) } + var updatedTodo by remember { mutableStateOf(todo) } Card( modifier = Modifier.fillMaxWidth().padding( @@ -45,22 +45,22 @@ fun EditableBookCard( Column( modifier = Modifier.padding(8.dp) ) { - TitleTextField( - title = updatedBook.title, - onUpdateTitle = { newTitle -> - updatedBook = updatedBook.copy( - title = newTitle + NameTextField( + name = updatedTodo.name, + onUpdateName = { newName -> + updatedTodo = updatedTodo.copy( + name = newName ) } ) Spacer( modifier = Modifier.height(8.dp) ) - AuthorTextField( - author = updatedBook.author, - onUpdateAuthor = { newAuthor -> - updatedBook = updatedBook.copy( - author = newAuthor + DescriptionTextField( + description = updatedTodo.description, + onUpdateDescription = { newDescription -> + updatedTodo = updatedTodo.copy( + description = newDescription ) } ) @@ -74,16 +74,16 @@ fun EditableBookCard( ) ActionButton( onActionButtonClick = { - updatedBook.apply { - if (title.isEmpty()) { - onEmptyBookField(TITLE_FIELD) - } else if (author.isEmpty()) { - onEmptyBookField(AUTHOR_FIELD) + updatedTodo.apply { + if (name.isEmpty()) { + onEmptyTodoField(NAME_FIELD) + } else if (description.isEmpty()) { + onEmptyTodoField(DESCRIPTION_FIELD) } else { - if (updatedBook != book) { - onUpdateBook(updatedBook) + if (updatedTodo != todo) { + onUpdateTodo(updatedTodo) } else { - onNoBookUpdates() + onNoTodoUpdates() } } } diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/EmptyBookListContent.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/EmptyTodoListContent.kt similarity index 84% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/EmptyBookListContent.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/EmptyTodoListContent.kt index 535ba97..1973634 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/EmptyBookListContent.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/EmptyTodoListContent.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_list.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -13,7 +13,7 @@ import androidx.compose.ui.unit.sp import ro.alexmamo.roomjetpackcompose.R @Composable -fun EmptyBookListContent( +fun EmptyTodoListContent( innerPadding: PaddingValues ) { Box( @@ -22,7 +22,7 @@ fun EmptyBookListContent( ){ Text( text = stringResource( - id = R.string.empty_book_list_text + id = R.string.empty_todo_list_text ), fontSize = 18.sp ) diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookFloatingActionButton.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/InsertFloatingActionButton.kt similarity index 70% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookFloatingActionButton.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/InsertFloatingActionButton.kt index 769b49a..a48ae83 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookFloatingActionButton.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/InsertFloatingActionButton.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_list.components import androidx.compose.material.FloatingActionButton import androidx.compose.material.Icon @@ -10,17 +10,17 @@ import androidx.compose.ui.res.stringResource import ro.alexmamo.roomjetpackcompose.R @Composable -fun InsertBookFloatingActionButton( - onInsertBookFloatingActionButtonClick: () -> Unit +fun InsertFloatingActionButton( + onInsertFloatingActionButtonClick: () -> Unit ) { FloatingActionButton( backgroundColor = MaterialTheme.colors.primary, - onClick = onInsertBookFloatingActionButtonClick + onClick = onInsertFloatingActionButtonClick ) { Icon( imageVector = Icons.Default.Add, contentDescription = stringResource( - id = R.string.open_insert_book_dialog + id = R.string.open_insert_todo_dialog ) ) } diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookAlertDialog.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/InsertTodoAlertDialog.kt similarity index 53% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookAlertDialog.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/InsertTodoAlertDialog.kt index 01b4b50..48a1ecb 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/InsertBookAlertDialog.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/InsertTodoAlertDialog.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_list.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -15,45 +15,45 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ro.alexmamo.roomjetpackcompose.R import ro.alexmamo.roomjetpackcompose.components.ActionButton -import ro.alexmamo.roomjetpackcompose.core.AUTHOR_FIELD -import ro.alexmamo.roomjetpackcompose.core.TITLE_FIELD -import ro.alexmamo.roomjetpackcompose.domain.model.Book +import ro.alexmamo.roomjetpackcompose.core.DESCRIPTION_FIELD +import ro.alexmamo.roomjetpackcompose.core.NAME_FIELD +import ro.alexmamo.roomjetpackcompose.domain.model.Todo const val EMPTY_STRING = "" @Composable -fun InsertBookAlertDialog( - onInsertBook: (book: Book) -> Unit, - onEmptyBookField: (String) -> Unit, - onInsertBookDialogCancel: () -> Unit, +fun InsertTodoAlertDialog( + onInsertTodo: (todo: Todo) -> Unit, + onEmptyTodoField: (String) -> Unit, + onInsertTodoDialogCancel: () -> Unit, ) { - var title by remember { mutableStateOf(EMPTY_STRING) } - var author by remember { mutableStateOf(EMPTY_STRING) } + var name by remember { mutableStateOf(EMPTY_STRING) } + var description by remember { mutableStateOf(EMPTY_STRING) } AlertDialog( - onDismissRequest = onInsertBookDialogCancel, + onDismissRequest = onInsertTodoDialogCancel, title = { Text( text = stringResource( - id = R.string.insert_book + id = R.string.insert_todo ) ) }, text = { Column { - TitleTextField( - title = title, - onUpdateTitle = { newTitle -> - title = newTitle + NameTextField( + name = name, + onUpdateName = { newName -> + name = newName } ) Spacer( modifier = Modifier.height(16.dp) ) - AuthorTextField( - author = author, - onUpdateAuthor = { newAuthor -> - author = newAuthor + DescriptionTextField( + description = description, + onUpdateDescription = { newDescription -> + description = newDescription } ) } @@ -61,29 +61,29 @@ fun InsertBookAlertDialog( confirmButton = { ActionButton( onActionButtonClick = { - if (title.isEmpty()) { - onEmptyBookField(TITLE_FIELD) + if (name.isEmpty()) { + onEmptyTodoField(NAME_FIELD) return@ActionButton } - if (author.isEmpty()) { - onEmptyBookField(AUTHOR_FIELD) + if (description.isEmpty()) { + onEmptyTodoField(DESCRIPTION_FIELD) return@ActionButton } - onInsertBook(Book( + onInsertTodo(Todo( id = 0, - title = title, - author = author + name = name, + description = description )) - onInsertBookDialogCancel() + onInsertTodoDialogCancel() }, resourceId = R.string.insert_button ) }, dismissButton = { ActionButton( - onActionButtonClick = onInsertBookDialogCancel, + onActionButtonClick = onInsertTodoDialogCancel, resourceId = R.string.cancel_button ) - } + }, ) } \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/TitleText.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/NameText.kt similarity index 79% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/TitleText.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/NameText.kt index cbe2c81..97b4650 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/TitleText.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/NameText.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_list.components import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -6,7 +6,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.sp @Composable -fun TitleText( +fun NameText( title: String ) { Text( diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/TitleTextField.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/NameTextField.kt similarity index 70% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/TitleTextField.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/NameTextField.kt index 5f07134..d078fea 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/TitleTextField.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/NameTextField.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_list.components import androidx.compose.material.Text import androidx.compose.material.TextField @@ -17,13 +17,13 @@ import androidx.compose.ui.text.input.TextFieldValue import ro.alexmamo.roomjetpackcompose.R @Composable -fun TitleTextField( - title: String, - onUpdateTitle: (String) -> Unit +fun NameTextField( + name: String, + onUpdateName: (String) -> Unit ) { - var title by remember { mutableStateOf(TextFieldValue( - text = title, - selection = TextRange(title.length) + var name by remember { mutableStateOf(TextFieldValue( + text = name, + selection = TextRange(name.length) )) } val focusRequester = remember { FocusRequester() } @@ -33,15 +33,15 @@ fun TitleTextField( TextField( modifier = Modifier.focusRequester(focusRequester), - value = title, - onValueChange = { newTitle -> - title = newTitle - onUpdateTitle(newTitle.text) + value = name, + onValueChange = { newName -> + name = newName + onUpdateName(newName.text) }, placeholder = { Text( text = stringResource( - id = R.string.book_title + id = R.string.todo_name ) ) } diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookCard.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/TodoCard.kt similarity index 51% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookCard.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/TodoCard.kt index 5cbacca..0d336b2 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookCard.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/TodoCard.kt @@ -1,6 +1,7 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_list.components import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -13,17 +14,18 @@ import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ro.alexmamo.roomjetpackcompose.R import ro.alexmamo.roomjetpackcompose.components.ActionIconButton -import ro.alexmamo.roomjetpackcompose.domain.model.Book +import ro.alexmamo.roomjetpackcompose.domain.model.Todo @Composable -fun BookCard( - book: Book, - onBookCardClick: () -> Unit, - onEditBook: () -> Unit, - onDeleteBook: () -> Unit +fun TodoCard( + todo: Todo, + onTodoCardClick: () -> Unit, + onEditTodo: () -> Unit, + onDeleteTodo: () -> Unit ) { Card( modifier = Modifier.fillMaxWidth().padding( @@ -32,7 +34,7 @@ fun BookCard( end = 8.dp, bottom = 4.dp ).clickable { - onBookCardClick() + onTodoCardClick() }, shape = MaterialTheme.shapes.small, elevation = 3.dp @@ -41,25 +43,37 @@ fun BookCard( modifier = Modifier.fillMaxWidth().padding(8.dp) ) { Column { - TitleText( - title = book.title + NameText( + title = todo.name ) - AuthorText( - author = book.author + DescriptionText( + author = todo.description ) } Spacer( modifier = Modifier.weight(1f) ) ActionIconButton( - onActionIconButtonClick = onEditBook, - imageVector = Icons.Default.Edit, - resourceId = R.string.edit_icon + onActionIconButtonClick = onEditTodo, + withCircle = false, + content = { mod -> + androidx.compose.material.Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(id = R.string.edit_icon), + modifier = mod + ) + } ) ActionIconButton( - onActionIconButtonClick = onDeleteBook, - imageVector = Icons.Default.Delete, - resourceId = R.string.delete_icon + onActionIconButtonClick = onDeleteTodo, + withCircle = false, + content = { mod -> + androidx.compose.material.Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(id = R.string.delete_icon), + modifier = mod + ) + } ) } } diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/TodoListContent.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/TodoListContent.kt new file mode 100644 index 0000000..af1b443 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/TodoListContent.kt @@ -0,0 +1,72 @@ +package ro.alexmamo.roomjetpackcompose.presentation.todo_list.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import ro.alexmamo.roomjetpackcompose.domain.model.Todo + +const val NON_EXISTENT_TODO_ID = -1 + +@Composable +fun TodoListContent( + innerPadding: PaddingValues, + todoList: List, + onTodoCardClick: (Todo) -> Unit, + onUpdateTodo: (Todo) -> Unit, + onEmptyTodoField: (String) -> Unit, + onDeleteTodo: (Todo) -> Unit, + onNoTodoUpdates: () -> Unit +) { + var editTodoId by remember { mutableIntStateOf(NON_EXISTENT_TODO_ID) } + + LazyColumn( + modifier = Modifier.fillMaxSize().padding(innerPadding) + ) { + items( + items = todoList, + key = { todo -> + todo.id + } + ) { todo -> + if (editTodoId != todo.id) { + TodoCard( + todo = todo, + onTodoCardClick = { + onTodoCardClick(todo) + }, + onEditTodo = { + editTodoId = todo.id + }, + onDeleteTodo = { + onDeleteTodo(todo) + editTodoId = NON_EXISTENT_TODO_ID + } + ) + } else { + EditableTodoCard( + todo = todo, + onUpdateTodo = { updatedTodo -> + onUpdateTodo(updatedTodo) + editTodoId = NON_EXISTENT_TODO_ID + }, + onEmptyTodoField = onEmptyTodoField, + onNoTodoUpdates = { + onNoTodoUpdates() + editTodoId = NON_EXISTENT_TODO_ID + }, + onCancel = { + editTodoId = NON_EXISTENT_TODO_ID + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListTopBar.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/TodoListTopBar.kt similarity index 72% rename from app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListTopBar.kt rename to app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/TodoListTopBar.kt index 852ccaa..1a7e6a0 100644 --- a/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/book_list/components/BookListTopBar.kt +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/presentation/todo_list/components/TodoListTopBar.kt @@ -1,4 +1,4 @@ -package ro.alexmamo.roomjetpackcompose.presentation.book_list.components +package ro.alexmamo.roomjetpackcompose.presentation.todo_list.components import androidx.compose.material.Text import androidx.compose.material.TopAppBar @@ -7,12 +7,12 @@ import androidx.compose.ui.res.stringResource import ro.alexmamo.roomjetpackcompose.R @Composable -fun BookListTopBar() { +fun TodoListTopBar() { TopAppBar ( title = { Text( text = stringResource( - id = R.string.book_list_screen_title + id = R.string.todo_list_screen_name ) ) } diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Color.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Color.kt new file mode 100644 index 0000000..70345de --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Color.kt @@ -0,0 +1,21 @@ +package ro.alexmamo.roomjetpackcompose.ui.theme + +import androidx.compose.ui.graphics.Color + +val Honeydew = Color(0xFFF1FFF3) + +val LightGreen = Color(0xFFDFF7E2) + +val CaribbeanGreen = Color(0xFF00D09E) + +val Cyprus = Color(0xFF0E3E3E) + +val FenceGreen = Color(0xFF052224) + +val Void = Color(0xFF031314) + +val LightBlue = Color(0xFF6DB6FE) + +val VividBlue = Color(0xFF3299FF) + +var OceanBlue = Color(0xFF0068FF) \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Dimens.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Dimens.kt new file mode 100644 index 0000000..0160d9a --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Dimens.kt @@ -0,0 +1,23 @@ +package ro.alexmamo.roomjetpackcompose.ui.theme + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +object Dimens { + // paddings + val paddingExtraSmall: Dp = 4.dp + val paddingSmall: Dp = 8.dp + val paddingMedium: Dp = 16.dp + val paddingLarge: Dp = 24.dp + val paddingExtraLarge: Dp = 32.dp + + // corners + val cornerSmall: Dp = 8.dp + val cornerMedium: Dp = 16.dp + val cornerLarge: Dp = 32.dp + + // sizes + val topBarHeight: Dp = 56.dp + val fabSize: Dp = 56.dp +} + diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Theme.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Theme.kt new file mode 100644 index 0000000..79c64b8 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package ro.alexmamo.roomjetpackcompose.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + background = FenceGreen, + surface = Cyprus, + onPrimary = Void, + onSecondary = Honeydew +) + +private val LightColorScheme = lightColorScheme( + background = CaribbeanGreen, + surface = LightGreen, + onPrimary = Void, + onSecondary = Void + + /* Other default colors to override + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun CustomTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = false, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Type.kt b/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Type.kt new file mode 100644 index 0000000..fe32699 --- /dev/null +++ b/app/src/main/java/ro/alexmamo/roomjetpackcompose/ui/theme/Type.kt @@ -0,0 +1,27 @@ +package ro.alexmamo.roomjetpackcompose.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import ro.alexmamo.roomjetpackcompose.R + +val Poppins = FontFamily( + Font(R.font.poppins_regular, FontWeight.Normal), + Font(R.font.poppins_medium, FontWeight.Medium), + Font(R.font.poppins_semi_bold, FontWeight.SemiBold), + Font(R.font.poppins_bold, FontWeight.Bold) +) + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = Poppins, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ) +) \ No newline at end of file diff --git a/app/src/main/res/drawable/analysis.xml b/app/src/main/res/drawable/analysis.xml new file mode 100644 index 0000000..f0b155f --- /dev/null +++ b/app/src/main/res/drawable/analysis.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/arrow_down.xml b/app/src/main/res/drawable/arrow_down.xml new file mode 100644 index 0000000..a96eb60 --- /dev/null +++ b/app/src/main/res/drawable/arrow_down.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/arrow_left.xml b/app/src/main/res/drawable/arrow_left.xml new file mode 100644 index 0000000..ec32681 --- /dev/null +++ b/app/src/main/res/drawable/arrow_left.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/calendar.xml b/app/src/main/res/drawable/calendar.xml new file mode 100644 index 0000000..8ce471a --- /dev/null +++ b/app/src/main/res/drawable/calendar.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/car.xml b/app/src/main/res/drawable/car.xml new file mode 100644 index 0000000..576c6ec --- /dev/null +++ b/app/src/main/res/drawable/car.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/check.xml b/app/src/main/res/drawable/check.xml new file mode 100644 index 0000000..e2278f8 --- /dev/null +++ b/app/src/main/res/drawable/check.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/check_progress.xml b/app/src/main/res/drawable/check_progress.xml new file mode 100644 index 0000000..f5d83bc --- /dev/null +++ b/app/src/main/res/drawable/check_progress.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/dollar.xml b/app/src/main/res/drawable/dollar.xml new file mode 100644 index 0000000..b2c1d81 --- /dev/null +++ b/app/src/main/res/drawable/dollar.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/entertainment.xml b/app/src/main/res/drawable/entertainment.xml new file mode 100644 index 0000000..61bdb72 --- /dev/null +++ b/app/src/main/res/drawable/entertainment.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/expense.xml b/app/src/main/res/drawable/expense.xml new file mode 100644 index 0000000..bcccdef --- /dev/null +++ b/app/src/main/res/drawable/expense.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/expenses.xml b/app/src/main/res/drawable/expenses.xml new file mode 100644 index 0000000..f61a85c --- /dev/null +++ b/app/src/main/res/drawable/expenses.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/eye_off.xml b/app/src/main/res/drawable/eye_off.xml new file mode 100644 index 0000000..8333687 --- /dev/null +++ b/app/src/main/res/drawable/eye_off.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/eye_on.xml b/app/src/main/res/drawable/eye_on.xml new file mode 100644 index 0000000..4a4b265 --- /dev/null +++ b/app/src/main/res/drawable/eye_on.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/facebook.xml b/app/src/main/res/drawable/facebook.xml new file mode 100644 index 0000000..61e1a70 --- /dev/null +++ b/app/src/main/res/drawable/facebook.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/fingerprint.xml b/app/src/main/res/drawable/fingerprint.xml new file mode 100644 index 0000000..89f26cb --- /dev/null +++ b/app/src/main/res/drawable/fingerprint.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/food.xml b/app/src/main/res/drawable/food.xml new file mode 100644 index 0000000..b8ebd1f --- /dev/null +++ b/app/src/main/res/drawable/food.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/google.xml b/app/src/main/res/drawable/google.xml new file mode 100644 index 0000000..d0b12dc --- /dev/null +++ b/app/src/main/res/drawable/google.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/groceries.xml b/app/src/main/res/drawable/groceries.xml new file mode 100644 index 0000000..8009b31 --- /dev/null +++ b/app/src/main/res/drawable/groceries.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/help.xml b/app/src/main/res/drawable/help.xml new file mode 100644 index 0000000..c51147d --- /dev/null +++ b/app/src/main/res/drawable/help.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/home.xml b/app/src/main/res/drawable/home.xml new file mode 100644 index 0000000..8e07216 --- /dev/null +++ b/app/src/main/res/drawable/home.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/income.xml b/app/src/main/res/drawable/income.xml new file mode 100644 index 0000000..6c316aa --- /dev/null +++ b/app/src/main/res/drawable/income.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/launch_1.xml b/app/src/main/res/drawable/launch_1.xml new file mode 100644 index 0000000..12ea0f3 --- /dev/null +++ b/app/src/main/res/drawable/launch_1.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/launch_2.xml b/app/src/main/res/drawable/launch_2.xml new file mode 100644 index 0000000..e4e67b0 --- /dev/null +++ b/app/src/main/res/drawable/launch_2.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/logout.xml b/app/src/main/res/drawable/logout.xml new file mode 100644 index 0000000..8bff9db --- /dev/null +++ b/app/src/main/res/drawable/logout.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/medicine.xml b/app/src/main/res/drawable/medicine.xml new file mode 100644 index 0000000..1af5317 --- /dev/null +++ b/app/src/main/res/drawable/medicine.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/more.xml b/app/src/main/res/drawable/more.xml new file mode 100644 index 0000000..90bb90d --- /dev/null +++ b/app/src/main/res/drawable/more.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/new_house.xml b/app/src/main/res/drawable/new_house.xml new file mode 100644 index 0000000..889c376 --- /dev/null +++ b/app/src/main/res/drawable/new_house.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/notification.xml b/app/src/main/res/drawable/notification.xml new file mode 100644 index 0000000..f04faa6 --- /dev/null +++ b/app/src/main/res/drawable/notification.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/profile.xml b/app/src/main/res/drawable/profile.xml new file mode 100644 index 0000000..5ee0aa0 --- /dev/null +++ b/app/src/main/res/drawable/profile.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/rent.xml b/app/src/main/res/drawable/rent.xml new file mode 100644 index 0000000..8394517 --- /dev/null +++ b/app/src/main/res/drawable/rent.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/src/main/res/drawable/salary.xml b/app/src/main/res/drawable/salary.xml new file mode 100644 index 0000000..ec0af54 --- /dev/null +++ b/app/src/main/res/drawable/salary.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/salary_white.xml b/app/src/main/res/drawable/salary_white.xml new file mode 100644 index 0000000..f80c7e5 --- /dev/null +++ b/app/src/main/res/drawable/salary_white.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/savings.xml b/app/src/main/res/drawable/savings.xml new file mode 100644 index 0000000..eb23f02 --- /dev/null +++ b/app/src/main/res/drawable/savings.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/security.xml b/app/src/main/res/drawable/security.xml new file mode 100644 index 0000000..01b50fb --- /dev/null +++ b/app/src/main/res/drawable/security.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/setting.xml b/app/src/main/res/drawable/setting.xml new file mode 100644 index 0000000..b232049 --- /dev/null +++ b/app/src/main/res/drawable/setting.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/star.xml b/app/src/main/res/drawable/star.xml new file mode 100644 index 0000000..8186c58 --- /dev/null +++ b/app/src/main/res/drawable/star.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/transactions.xml b/app/src/main/res/drawable/transactions.xml new file mode 100644 index 0000000..4419f1d --- /dev/null +++ b/app/src/main/res/drawable/transactions.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/transport.xml b/app/src/main/res/drawable/transport.xml new file mode 100644 index 0000000..ffe572d --- /dev/null +++ b/app/src/main/res/drawable/transport.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/travel.xml b/app/src/main/res/drawable/travel.xml new file mode 100644 index 0000000..bc14e14 --- /dev/null +++ b/app/src/main/res/drawable/travel.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/wedding.xml b/app/src/main/res/drawable/wedding.xml new file mode 100644 index 0000000..b848d8b --- /dev/null +++ b/app/src/main/res/drawable/wedding.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/font/poppins_bold.otf b/app/src/main/res/font/poppins_bold.otf new file mode 100644 index 0000000..3016454 Binary files /dev/null and b/app/src/main/res/font/poppins_bold.otf differ diff --git a/app/src/main/res/font/poppins_medium.otf b/app/src/main/res/font/poppins_medium.otf new file mode 100644 index 0000000..49e7b6b Binary files /dev/null and b/app/src/main/res/font/poppins_medium.otf differ diff --git a/app/src/main/res/font/poppins_regular.otf b/app/src/main/res/font/poppins_regular.otf new file mode 100644 index 0000000..e5c4eee Binary files /dev/null and b/app/src/main/res/font/poppins_regular.otf differ diff --git a/app/src/main/res/font/poppins_semi_bold.otf b/app/src/main/res/font/poppins_semi_bold.otf new file mode 100644 index 0000000..fcd0845 Binary files /dev/null and b/app/src/main/res/font/poppins_semi_bold.otf differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ac9b94..957b27d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,13 +1,13 @@ RoomJetpackCompose - books_db - - "Book List" - "Book Details" + todos_db + + "Todo List" + "Todo Details" - "The book list is empty." - "Insert book" + "The todo list is empty." + "Insert todo" "Insert" "Update" @@ -16,17 +16,31 @@ "Edit" Delete - "Open insert book dialog." + "Open insert todo dialog." Navigate back + + Notification + Today + Yesterday + This Weekend + + Reminder! + Set up your automatic savings to meet your savings goal... + New Update + Transactions + A new transaction has been registered\nGroceries | Pantry | -$100,00 + Expense record + We recommend that you be more attentive to your finances. + 17:00 - April 24 - "Type a book title…" - "Type a book author…" + "Type a todo name…" + "Type a todo description…" - "Book %1$s cannot be empty." - "No book updates performed." - "Book successfully %1$s." + "Todo %1$s cannot be empty." + "No todo updates performed." + "Todo successfully %1$s." - "Title Test" - "Author Test" - "New Title Test" + "Name Test" + "Description Test" + "New Name Test" \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c575f53..fd9b33f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +coilCompose = "2.7.0" gradle = "8.8.1" kotlin = "2.1.10" ksp = "2.1.10-1.0.30" @@ -7,6 +8,8 @@ composeBom = "2025.02.00" compose = "1.5.15" navigationCompose = "2.8.7" hiltNavigationCompose = "1.2.0" +retrofit = "3.0.0" +converterGson = "3.0.0" room = "2.6.1" serialization = "1.7.3" #Tests @@ -15,6 +18,11 @@ uiTestJunit4 = "1.7.8" kotlinxCoroutinesTest = "1.9.0" truth = "1.1.3" uiTestManifest = "1.7.8" +material3 = "1.4.0" +foundation = "1.9.4" +ui = "1.9.4" +lifecycleViewmodelKtx = "2.9.4" +runtime = "1.9.4" [plugins] android-application = { id = "com.android.application", version.ref = "gradle" } @@ -25,6 +33,7 @@ hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } [libraries] +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } compose-material = { module = "androidx.compose.material:material" } compose-material-icons = { module = "androidx.compose.material:material-icons-extended" } @@ -32,6 +41,8 @@ navigation-compose = { module = "androidx.navigation:navigation-compose", versio hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" } room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } @@ -43,4 +54,9 @@ runner = { module = "androidx.test:runner", version.ref = "runner" } ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "uiTestJunit4" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } truth = { module = "com.google.truth:truth", version.ref = "truth" } -ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "uiTestManifest" } \ No newline at end of file +ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "uiTestManifest" } +material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } +ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" } +lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } +runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }