From 1e5e727e92758baab52dbd4fc8b1e5f1a47352f3 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 3 Jul 2023 15:07:19 +0200 Subject: [PATCH] Implement shopping lists screen (#129) * Initialize shopping lists feature * Start shopping lists screen with Compose * Add icon to shopping list name * Add shopping lists to menu * Set max size for the list * Replace compose-adapter with accompanist * Remove unused fields from shopping lists response * Show list of shopping lists from BE * Hide shopping lists if Mealie is 0.5.6 * Add shopping list item click listener * Create material app theme for Compose * Use shorter names * Load shopping lists by pages and save to db * Make page handling logic match recipes * Add swipe to refresh to shopping lists * Extract SwipeToRefresh Composable * Make LazyPagingColumn generic * Show refresh only when mediator is refreshing * Do not refresh automatically * Allow controlling Activity state from modules * Implement navigating to shopping list screen * Move Compose libraries setup to a plugin * Implement loading full shopping list info * Move Storage classes to database module * Save shopping list items to DB * Use separate names for separate ids * Do only one DB version update * Use unique names for all columns * Display shopping list items * Move OperationUiState to ui module * Subscribe to shopping lists updates * Indicate progress with progress bar * Use strings from resources * Format shopping list item quantities * Hide unit/food/note/quantity if they are not set * Implement updating shopping list item checked state * Remove unnecessary null checks * Disable checkbox when it is being updated * Split shopping list screen into composables * Show items immediately if they are saved * Fix showing "list is empty" before the items * Show Snackbar when error happens * Reduce shopping list items paddings * Remove shopping lists when URL is changed * Add error/empty state handling to shopping lists * Fix empty error state * Fix tests compilation * Add margin between text and button * Add divider between checked and unchecked items * Move divider to the item * Refresh the shopping lists on authentication * Use retry when necessary * Remove excessive logging * Fix pages bounds check * Move FlowExtensionsTest * Update Compose version * Fix showing loading indicator for shopping lists * Add Russian translation * Fix SDK version lint check * Rename parameter to match interface * Add DB migration TODO * Get rid of DB migrations * Do not use pagination with shopping lists * Cleanup after the pagination removal * Load shopping list items * Remove shopping lists storage * Rethrow CancellationException in LoadingHelper * Add pull-to-refresh on shopping list screen * Extract LazyColumnWithLoadingState * Split refresh errors and loading state * Reuse LazyColumnWithLoadingState for shopping list items * Remove paging-compose dependency * Refresh shopping list items on authentication * Disable missing translation lint check * Update Compose and Kotlin versions * Fix order of checked items * Hide useless information from a shopping list item --- app/build.gradle.kts | 6 + .../mealient/data/add/AddRecipeDataSource.kt | 2 + .../mealient/data/add/AddRecipeRepo.kt | 1 + .../data/add/impl/AddRecipeRepoImpl.kt | 10 +- .../kirmanak/mealient/data/auth/AuthRepo.kt | 5 +- .../mealient/data/baseurl/ServerInfoRepo.kt | 4 + .../data/baseurl/ServerInfoRepoImpl.kt | 10 + .../data/baseurl/ServerInfoStorage.kt | 3 + .../data/baseurl/VersionDataSource.kt | 2 + .../data/baseurl/VersionDataSourceImpl.kt | 8 +- .../baseurl/impl/ServerInfoStorageImpl.kt | 5 + .../data/network/MealieDataSourceWrapper.kt | 34 +- .../mealient/data/recipes/RecipeRepo.kt | 4 +- .../mealient/data/recipes/db/RecipeStorage.kt | 24 - .../impl/RecipePagingSourceFactoryImpl.kt | 2 +- .../data/recipes/impl/RecipeRepoImpl.kt | 18 +- .../recipes/impl/RecipesRemoteMediator.kt | 7 +- .../data/recipes/network/RecipeDataSource.kt | 3 + .../data/share/ParseRecipeDataSource.kt | 2 + .../data/share/ShareRecipeRepoImpl.kt | 1 + .../gq/kirmanak/mealient/di/AuthModule.kt | 5 + .../gq/kirmanak/mealient/di/RecipeModule.kt | 6 - .../mealient/extensions/ModelMappings.kt | 224 --------- .../mealient/ui/activity/MainActivity.kt | 19 +- .../ui/activity/MainActivityUiState.kt | 14 - .../ui/activity/MainActivityViewModel.kt | 25 +- .../mealient/ui/add/AddRecipeFragment.kt | 11 +- .../mealient/ui/add/AddRecipeViewModel.kt | 2 +- .../ui/auth/AuthenticationFragment.kt | 7 +- .../mealient/ui/baseurl/BaseURLFragment.kt | 3 +- .../mealient/ui/baseurl/BaseURLViewModel.kt | 2 + .../ui/disclaimer/DisclaimerFragment.kt | 6 +- .../ui/recipes/RecipesListFragment.kt | 4 +- .../ui/recipes/RecipesListViewModel.kt | 2 +- app/src/main/res/menu/navigation_menu.xml | 7 + app/src/main/res/navigation/nav_graph.xml | 10 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../data/add/impl/AddRecipeRepoTest.kt | 10 +- .../data/baseurl/ServerInfoRepoTest.kt | 3 +- .../network/MealieDataSourceWrapperTest.kt | 26 +- .../data/recipes/db/RecipeStorageImplTest.kt | 97 ---- .../impl/RecipePagingSourceFactoryImplTest.kt | 8 +- .../data/recipes/impl/RecipeRepoTest.kt | 39 +- .../recipes/impl/RecipesRemoteMediatorTest.kt | 11 +- .../data/share/ShareRecipeRepoImplTest.kt | 1 + .../mealient/extensions/ModelMappingsTest.kt | 146 ------ .../mealient/test/RecipeImplTestData.kt | 440 ------------------ .../ui/activity/MainActivityViewModelTest.kt | 11 + .../mealient/ui/add/AddRecipeViewModelTest.kt | 2 +- .../ui/baseurl/BaseURLViewModelTest.kt | 5 + .../ui/recipes/RecipesListViewModelTest.kt | 2 +- .../recipes/info/RecipeInfoViewModelTest.kt | 2 +- architecture/build.gradle.kts | 6 + .../mealient/architecture}/FlowExtensions.kt | 2 +- .../architecture}/FlowExtensionsKtTest.kt | 2 +- build-logic/convention/build.gradle.kts | 4 + .../AndroidLibraryComposeConventionPlugin.kt | 16 + .../gq/kirmanak/mealient/AndroidCompose.kt | 74 +++ .../gq/kirmanak/mealient/KotlinAndroid.kt | 6 +- database/build.gradle.kts | 15 +- .../1.json | 404 ---------------- .../2.json | 404 ---------------- .../3.json | 404 ---------------- .../4.json | 410 ---------------- .../5.json | 374 --------------- .../6.json | 160 ------- .../7.json | 191 -------- .../8.json | 198 -------- .../gq/kirmanak/mealient/database/AppDb.kt | 31 +- .../mealient/database/DatabaseModule.kt | 16 +- .../mealient/database/recipe/RecipeDao.kt | 37 +- .../mealient/database/recipe/RecipeStorage.kt | 30 ++ .../database/recipe}/RecipeStorageImpl.kt | 38 +- .../database/recipe/entity/RecipeEntity.kt | 4 +- .../recipe/entity/RecipeIngredientEntity.kt | 27 +- .../recipe/entity/RecipeInstructionEntity.kt | 19 +- .../recipe/entity/RecipeSummaryEntity.kt | 19 +- ...thSummaryAndIngredientsAndInstructions.kt} | 10 +- .../database/RecipeStorageImplTest.kt | 113 +++++ database_test/.gitignore | 1 + database_test/build.gradle.kts | 11 + .../gq/kirmanak/mealient/database/TestData.kt | 128 +++++ datasource/build.gradle.kts | 2 +- .../datasource/models}/AddRecipeInfo.kt | 2 +- .../datasource/models}/FullRecipeInfo.kt | 2 +- .../datasource/models/FullShoppingListInfo.kt | 28 ++ .../datasource/models}/ParseRecipeURLInfo.kt | 2 +- .../datasource/models}/RecipeSummaryInfo.kt | 2 +- .../datasource/models/ShoppingListsInfo.kt | 14 + .../datasource/models}/VersionInfo.kt | 2 +- .../datasource/v1/MealieDataSourceV1.kt | 9 + .../datasource/v1/MealieDataSourceV1Impl.kt | 57 ++- .../mealient/datasource/v1/MealieServiceV1.kt | 23 + .../GetRecipeIngredientFoodResponseV1.kt | 9 + .../GetRecipeIngredientUnitResponseV1.kt | 9 + .../v1/models/GetRecipeResponseV1.kt | 12 +- .../v1/models/GetShoppingListResponseV1.kt | 42 ++ .../v1/models/GetShoppingListsResponseV1.kt | 13 + .../GetShoppingListsSummaryResponseV1.kt | 10 + datasource_test/.gitignore | 1 + datasource_test/build.gradle.kts | 11 + .../mealient/datasource_test/TestData.kt | 301 ++++++++++++ datastore_test/.gitignore | 1 + datastore_test/build.gradle.kts | 11 + .../mealient/datastore_test/TestData.kt | 14 + features/shopping_lists/.gitignore | 1 + features/shopping_lists/build.gradle.kts | 45 ++ .../main/kotlin/gq/kirmanak/mealient/Theme.kt | 46 ++ .../shopping_lists/ShoppingListsModule.kt | 29 ++ .../network/ShoppingListsDataSource.kt | 13 + .../network/ShoppingListsDataSourceImpl.kt | 30 ++ .../repo/ShoppingListsAuthRepo.kt | 8 + .../shopping_lists/repo/ShoppingListsRepo.kt | 13 + .../repo/ShoppingListsRepoImpl.kt | 30 ++ .../mealient/shopping_lists/ui/MealientApp.kt | 18 + .../shopping_lists/ui/ShoppingListScreen.kt | 223 +++++++++ .../ui/ShoppingListScreenState.kt | 13 + .../ui/ShoppingListViewModel.kt | 114 +++++ .../ui/ShoppingListsFragment.kt | 47 ++ .../shopping_lists/ui/ShoppingListsScreen.kt | 96 ++++ .../ui/ShoppingListsViewModel.kt | 59 +++ .../composables/CenteredProgressIndicator.kt | 30 ++ .../ui/composables/CenteredText.kt | 31 ++ .../ui/composables/EmptyListError.kt | 56 +++ .../ui/composables/ErrorSnackbar.kt | 29 ++ .../ui/composables/GetErrorMessage.kt | 13 + .../ui/composables/LazyColumnPullRefresh.kt | 33 ++ .../composables/LazyColumnWithLoadingState.kt | 79 ++++ .../shopping_lists/util/LoadingHelper.kt | 10 + .../util/LoadingHelperFactory.kt | 8 + .../util/LoadingHelperFactoryImpl.kt | 17 + .../shopping_lists/util/LoadingHelperImpl.kt | 42 ++ .../shopping_lists/util/LoadingState.kt | 58 +++ .../main/res/drawable/ic_shopping_cart.xml | 10 + .../src/main/res/values-ru/strings.xml | 11 + .../src/main/res/values/strings.xml | 11 + gradle/libs.versions.toml | 34 +- model_mapper/.gitignore | 1 + model_mapper/build.gradle.kts | 30 ++ .../mealient/model_mapper/ModelMapper.kt | 130 ++++++ .../mealient/model_mapper/ModelMapperImpl.kt | 310 ++++++++++++ .../model_mapper/ModelMapperModule.kt | 16 + .../model_mapper/ModelMappingsTest.kt | 157 +++++++ settings.gradle.kts | 6 + template_module/.gitignore | 1 + template_module/build.gradle.kts | 10 + .../gq/kirmanak/mealient/test/FakeLogger.kt | 5 +- .../mealient/test/FakeLoggingModule.kt | 21 + .../mealient/test/HiltRobolectricTest.kt | 4 +- ui/.gitignore | 1 + ui/build.gradle.kts | 22 + .../kirmanak/mealient/ui/ActivityUiState.kt | 21 + .../mealient/ui/ActivityUiStateController.kt | 12 + .../ui/ActivityUiStateControllerImpl.kt | 21 + .../kirmanak/mealient/ui/OperationUiState.kt | 0 .../gq/kirmanak/mealient/ui/UiModule.kt | 16 + 157 files changed, 3360 insertions(+), 3715 deletions(-) delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/extensions/ModelMappings.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt delete mode 100644 app/src/test/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImplTest.kt delete mode 100644 app/src/test/java/gq/kirmanak/mealient/extensions/ModelMappingsTest.kt delete mode 100644 app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt rename {app/src/main/java/gq/kirmanak/mealient/extensions => architecture/src/main/kotlin/gq/kirmanak/mealient/architecture}/FlowExtensions.kt (93%) rename {app/src/test/java/gq/kirmanak/mealient/extensions => architecture/src/test/kotlin/gq/kirmanak/mealient/architecture}/FlowExtensionsKtTest.kt (97%) create mode 100644 build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/AndroidCompose.kt delete mode 100644 database/schemas/gq.kirmanak.mealient.database.AppDb/1.json delete mode 100644 database/schemas/gq.kirmanak.mealient.database.AppDb/2.json delete mode 100644 database/schemas/gq.kirmanak.mealient.database.AppDb/3.json delete mode 100644 database/schemas/gq.kirmanak.mealient.database.AppDb/4.json delete mode 100644 database/schemas/gq.kirmanak.mealient.database.AppDb/5.json delete mode 100644 database/schemas/gq.kirmanak.mealient.database.AppDb/6.json delete mode 100644 database/schemas/gq.kirmanak.mealient.database.AppDb/7.json delete mode 100644 database/schemas/gq.kirmanak.mealient.database.AppDb/8.json create mode 100644 database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeStorage.kt rename {app/src/main/java/gq/kirmanak/mealient/data/recipes/db => database/src/main/kotlin/gq/kirmanak/mealient/database/recipe}/RecipeStorageImpl.kt (66%) rename database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/{FullRecipeEntity.kt => RecipeWithSummaryAndIngredientsAndInstructions.kt} (69%) create mode 100644 database/src/test/kotlin/gq/kirmanak/mealient/database/RecipeStorageImplTest.kt create mode 100644 database_test/.gitignore create mode 100644 database_test/build.gradle.kts create mode 100644 database_test/src/main/kotlin/gq/kirmanak/mealient/database/TestData.kt rename {app/src/main/java/gq/kirmanak/mealient/data/add => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/AddRecipeInfo.kt (91%) rename {app/src/main/java/gq/kirmanak/mealient/data/recipes/network => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/FullRecipeInfo.kt (91%) create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/FullShoppingListInfo.kt rename {app/src/main/java/gq/kirmanak/mealient/data/share => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/ParseRecipeURLInfo.kt (64%) rename {app/src/main/java/gq/kirmanak/mealient/data/recipes/network => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/RecipeSummaryInfo.kt (85%) create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ShoppingListsInfo.kt rename {app/src/main/java/gq/kirmanak/mealient/data/baseurl => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/VersionInfo.kt (52%) create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeIngredientFoodResponseV1.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeIngredientUnitResponseV1.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListResponseV1.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListsResponseV1.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListsSummaryResponseV1.kt create mode 100644 datasource_test/.gitignore create mode 100644 datasource_test/build.gradle.kts create mode 100644 datasource_test/src/main/kotlin/gq/kirmanak/mealient/datasource_test/TestData.kt create mode 100644 datastore_test/.gitignore create mode 100644 datastore_test/build.gradle.kts create mode 100644 datastore_test/src/main/kotlin/gq/kirmanak/mealient/datastore_test/TestData.kt create mode 100644 features/shopping_lists/.gitignore create mode 100644 features/shopping_lists/build.gradle.kts create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/Theme.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ShoppingListsModule.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSource.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSourceImpl.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsAuthRepo.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepo.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepoImpl.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/MealientApp.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListScreen.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListScreenState.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListViewModel.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsFragment.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsScreen.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsViewModel.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/CenteredProgressIndicator.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/CenteredText.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/EmptyListError.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/ErrorSnackbar.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/GetErrorMessage.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/LazyColumnPullRefresh.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/LazyColumnWithLoadingState.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelper.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperFactory.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperFactoryImpl.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperImpl.kt create mode 100644 features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingState.kt create mode 100644 features/shopping_lists/src/main/res/drawable/ic_shopping_cart.xml create mode 100644 features/shopping_lists/src/main/res/values-ru/strings.xml create mode 100644 features/shopping_lists/src/main/res/values/strings.xml create mode 100644 model_mapper/.gitignore create mode 100644 model_mapper/build.gradle.kts create mode 100644 model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapper.kt create mode 100644 model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapperImpl.kt create mode 100644 model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapperModule.kt create mode 100644 model_mapper/src/test/kotlin/gq/kirmanak/mealient/model_mapper/ModelMappingsTest.kt create mode 100644 template_module/.gitignore create mode 100644 template_module/build.gradle.kts create mode 100644 testing/src/main/kotlin/gq/kirmanak/mealient/test/FakeLoggingModule.kt create mode 100644 ui/.gitignore create mode 100644 ui/build.gradle.kts create mode 100644 ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiState.kt create mode 100644 ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiStateController.kt create mode 100644 ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiStateControllerImpl.kt rename {app/src/main/java => ui/src/main/kotlin}/gq/kirmanak/mealient/ui/OperationUiState.kt (100%) create mode 100644 ui/src/main/kotlin/gq/kirmanak/mealient/ui/UiModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0473ca1..e1a5003 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,9 +80,15 @@ dependencies { implementation(project(":architecture")) implementation(project(":database")) + testImplementation(project(":database_test")) implementation(project(":datastore")) + testImplementation(project(":datastore_test")) implementation(project(":datasource")) + testImplementation(project(":datasource_test")) implementation(project(":logging")) + implementation(project(":ui")) + implementation(project(":features:shopping_lists")) + implementation(project(":model_mapper")) testImplementation(project(":testing")) implementation(libs.android.material.material) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeDataSource.kt index dc5769b..405335d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeDataSource.kt @@ -1,5 +1,7 @@ package gq.kirmanak.mealient.data.add +import gq.kirmanak.mealient.datasource.models.AddRecipeInfo + interface AddRecipeDataSource { suspend fun addRecipe(recipe: AddRecipeInfo): String diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeRepo.kt index a0c7620..b682ae4 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeRepo.kt @@ -1,5 +1,6 @@ package gq.kirmanak.mealient.data.add +import gq.kirmanak.mealient.datasource.models.AddRecipeInfo import kotlinx.coroutines.flow.Flow interface AddRecipeRepo { diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt index 9043ca8..3003701 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt @@ -1,12 +1,11 @@ package gq.kirmanak.mealient.data.add.impl import gq.kirmanak.mealient.data.add.AddRecipeDataSource -import gq.kirmanak.mealient.data.add.AddRecipeInfo import gq.kirmanak.mealient.data.add.AddRecipeRepo +import gq.kirmanak.mealient.datasource.models.AddRecipeInfo import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage -import gq.kirmanak.mealient.extensions.toAddRecipeInfo -import gq.kirmanak.mealient.extensions.toDraft import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.model_mapper.ModelMapper import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -18,14 +17,15 @@ class AddRecipeRepoImpl @Inject constructor( private val addRecipeDataSource: AddRecipeDataSource, private val addRecipeStorage: AddRecipeStorage, private val logger: Logger, + private val modelMapper: ModelMapper, ) : AddRecipeRepo { override val addRecipeRequestFlow: Flow - get() = addRecipeStorage.updates.map { it.toAddRecipeInfo() } + get() = addRecipeStorage.updates.map { modelMapper.toAddRecipeInfo(it) } override suspend fun preserve(recipe: AddRecipeInfo) { logger.v { "preserveRecipe() called with: recipe = $recipe" } - addRecipeStorage.save(recipe.toDraft()) + addRecipeStorage.save(modelMapper.toDraft(recipe)) } override suspend fun clear() { diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt index c07f800..d06d75f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt @@ -1,10 +1,11 @@ package gq.kirmanak.mealient.data.auth +import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo import kotlinx.coroutines.flow.Flow -interface AuthRepo { +interface AuthRepo : ShoppingListsAuthRepo { - val isAuthorizedFlow: Flow + override val isAuthorizedFlow: Flow suspend fun authenticate(email: String, password: String) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepo.kt index 153b483..acaacb5 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepo.kt @@ -1,5 +1,7 @@ package gq.kirmanak.mealient.data.baseurl +import kotlinx.coroutines.flow.Flow + interface ServerInfoRepo { suspend fun getUrl(): String? @@ -8,5 +10,7 @@ interface ServerInfoRepo { suspend fun tryBaseURL(baseURL: String): Result + fun versionUpdates(): Flow + } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoImpl.kt index 97c9fb8..7a9c6f2 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoImpl.kt @@ -3,6 +3,9 @@ package gq.kirmanak.mealient.data.baseurl import gq.kirmanak.mealient.datasource.ServerUrlProvider import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map import javax.inject.Inject import javax.inject.Singleton @@ -55,4 +58,11 @@ class ServerInfoRepoImpl @Inject constructor( serverInfoStorage.storeBaseURL(oldBaseUrl, oldVersion) } } + + override fun versionUpdates(): Flow { + return serverInfoStorage + .serverVersionUpdates() + .filterNotNull() + .map { determineServerVersion(it) } + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoStorage.kt index 09b7f1a..f08f165 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoStorage.kt @@ -1,5 +1,7 @@ package gq.kirmanak.mealient.data.baseurl +import kotlinx.coroutines.flow.Flow + interface ServerInfoStorage { suspend fun getBaseURL(): String? @@ -12,4 +14,5 @@ interface ServerInfoStorage { suspend fun getServerVersion(): String? + fun serverVersionUpdates(): Flow } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt index 03909d9..c5a28a3 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt @@ -1,5 +1,7 @@ package gq.kirmanak.mealient.data.baseurl +import gq.kirmanak.mealient.datasource.models.VersionInfo + interface VersionDataSource { suspend fun getVersionInfo(): VersionInfo diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt index 76df788..4e451aa 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt @@ -1,9 +1,10 @@ package gq.kirmanak.mealient.data.baseurl +import gq.kirmanak.mealient.datasource.models.VersionInfo import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 -import gq.kirmanak.mealient.extensions.toVersionInfo +import gq.kirmanak.mealient.model_mapper.ModelMapper import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -14,15 +15,16 @@ import javax.inject.Singleton class VersionDataSourceImpl @Inject constructor( private val v0Source: MealieDataSourceV0, private val v1Source: MealieDataSourceV1, + private val modelMapper: ModelMapper, ) : VersionDataSource { override suspend fun getVersionInfo(): VersionInfo { val responses = coroutineScope { val v0Deferred = async { - runCatchingExceptCancel { v0Source.getVersionInfo().toVersionInfo() } + runCatchingExceptCancel { modelMapper.toVersionInfo(v0Source.getVersionInfo()) } } val v1Deferred = async { - runCatchingExceptCancel { v1Source.getVersionInfo().toVersionInfo() } + runCatchingExceptCancel { modelMapper.toVersionInfo(v1Source.getVersionInfo()) } } listOf(v0Deferred, v1Deferred).awaitAll() } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/ServerInfoStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/ServerInfoStorageImpl.kt index 70e1f27..7631f35 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/ServerInfoStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/ServerInfoStorageImpl.kt @@ -3,6 +3,7 @@ package gq.kirmanak.mealient.data.baseurl.impl import androidx.datastore.preferences.core.Preferences import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage import gq.kirmanak.mealient.data.storage.PreferencesStorage +import kotlinx.coroutines.flow.Flow import javax.inject.Inject import javax.inject.Singleton @@ -49,6 +50,10 @@ class ServerInfoStorageImpl @Inject constructor( preferencesStorage.storeValues(Pair(serverVersionKey, version)) } + override fun serverVersionUpdates(): Flow { + return preferencesStorage.valueUpdates(serverVersionKey) + } + private suspend fun getValue(key: Preferences.Key): T? = preferencesStorage.getValue(key) } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt index 1c75553..22302da 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt @@ -1,22 +1,17 @@ package gq.kirmanak.mealient.data.network import gq.kirmanak.mealient.data.add.AddRecipeDataSource -import gq.kirmanak.mealient.data.add.AddRecipeInfo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.ServerVersion -import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource -import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo import gq.kirmanak.mealient.data.share.ParseRecipeDataSource -import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeInfo +import gq.kirmanak.mealient.datasource.models.FullRecipeInfo +import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo +import gq.kirmanak.mealient.datasource.models.RecipeSummaryInfo import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 -import gq.kirmanak.mealient.extensions.toFullRecipeInfo -import gq.kirmanak.mealient.extensions.toRecipeSummaryInfo -import gq.kirmanak.mealient.extensions.toV0Request -import gq.kirmanak.mealient.extensions.toV1CreateRequest -import gq.kirmanak.mealient.extensions.toV1Request -import gq.kirmanak.mealient.extensions.toV1UpdateRequest +import gq.kirmanak.mealient.model_mapper.ModelMapper import javax.inject.Inject import javax.inject.Singleton @@ -25,15 +20,16 @@ class MealieDataSourceWrapper @Inject constructor( private val serverInfoRepo: ServerInfoRepo, private val v0Source: MealieDataSourceV0, private val v1Source: MealieDataSourceV1, + private val modelMapper: ModelMapper, ) : AddRecipeDataSource, RecipeDataSource, ParseRecipeDataSource { private suspend fun getVersion(): ServerVersion = serverInfoRepo.getVersion() override suspend fun addRecipe(recipe: AddRecipeInfo): String = when (getVersion()) { - ServerVersion.V0 -> v0Source.addRecipe(recipe.toV0Request()) + ServerVersion.V0 -> v0Source.addRecipe(modelMapper.toV0Request(recipe)) ServerVersion.V1 -> { - val slug = v1Source.createRecipe(recipe.toV1CreateRequest()) - v1Source.updateRecipe(slug, recipe.toV1UpdateRequest()) + val slug = v1Source.createRecipe(modelMapper.toV1CreateRequest(recipe)) + v1Source.updateRecipe(slug, modelMapper.toV1UpdateRequest(recipe)) slug } } @@ -43,25 +39,25 @@ class MealieDataSourceWrapper @Inject constructor( limit: Int, ): List = when (getVersion()) { ServerVersion.V0 -> { - v0Source.requestRecipes(start, limit).map { it.toRecipeSummaryInfo() } + v0Source.requestRecipes(start, limit).map { modelMapper.toRecipeSummaryInfo(it) } } ServerVersion.V1 -> { // Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3 val page = start / limit + 1 - v1Source.requestRecipes(page, limit).map { it.toRecipeSummaryInfo() } + v1Source.requestRecipes(page, limit).map { modelMapper.toRecipeSummaryInfo(it) } } } override suspend fun requestRecipeInfo(slug: String): FullRecipeInfo = when (getVersion()) { - ServerVersion.V0 -> v0Source.requestRecipeInfo(slug).toFullRecipeInfo() - ServerVersion.V1 -> v1Source.requestRecipeInfo(slug).toFullRecipeInfo() + ServerVersion.V0 -> modelMapper.toFullRecipeInfo(v0Source.requestRecipeInfo(slug)) + ServerVersion.V1 -> modelMapper.toFullRecipeInfo(v1Source.requestRecipeInfo(slug)) } override suspend fun parseRecipeFromURL( parseRecipeURLInfo: ParseRecipeURLInfo, ): String = when (getVersion()) { - ServerVersion.V0 -> v0Source.parseRecipeFromURL(parseRecipeURLInfo.toV0Request()) - ServerVersion.V1 -> v1Source.parseRecipeFromURL(parseRecipeURLInfo.toV1Request()) + ServerVersion.V0 -> v0Source.parseRecipeFromURL(modelMapper.toV0Request(parseRecipeURLInfo)) + ServerVersion.V1 -> v1Source.parseRecipeFromURL(modelMapper.toV1Request(parseRecipeURLInfo)) } override suspend fun getFavoriteRecipes(): List = when (getVersion()) { diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt index 22996f2..dffd4dc 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt @@ -1,8 +1,8 @@ package gq.kirmanak.mealient.data.recipes import androidx.paging.Pager -import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions interface RecipeRepo { @@ -12,7 +12,7 @@ interface RecipeRepo { suspend fun refreshRecipeInfo(recipeSlug: String): Result - suspend fun loadRecipeInfo(recipeId: String): FullRecipeEntity? + suspend fun loadRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions? fun updateNameQuery(name: String?) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt deleted file mode 100644 index e0f44e7..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt +++ /dev/null @@ -1,24 +0,0 @@ -package gq.kirmanak.mealient.data.recipes.db - -import androidx.paging.PagingSource -import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo -import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity -import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity - -interface RecipeStorage { - suspend fun saveRecipes(recipes: List) - - fun queryRecipes(query: String?): PagingSource - - suspend fun refreshAll(recipes: List) - - suspend fun clearAllLocalData() - - suspend fun saveRecipeInfo(recipe: FullRecipeInfo) - - suspend fun queryRecipeInfo(recipeId: String): FullRecipeEntity? - - suspend fun updateFavoriteRecipes(favorites: List) - - suspend fun deleteRecipe(entity: RecipeSummaryEntity) -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt index 35c5500..c65b80e 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImpl.kt @@ -1,7 +1,7 @@ package gq.kirmanak.mealient.data.recipes.impl import androidx.paging.InvalidatingPagingSourceFactory -import gq.kirmanak.mealient.data.recipes.db.RecipeStorage +import gq.kirmanak.mealient.database.recipe.RecipeStorage import gq.kirmanak.mealient.logging.Logger import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt index 5322775..79607e4 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt @@ -4,12 +4,13 @@ import androidx.paging.ExperimentalPagingApi import androidx.paging.Pager import androidx.paging.PagingConfig import gq.kirmanak.mealient.data.recipes.RecipeRepo -import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource -import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity +import gq.kirmanak.mealient.database.recipe.RecipeStorage import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.model_mapper.ModelMapper import javax.inject.Inject import javax.inject.Singleton @@ -21,6 +22,7 @@ class RecipeRepoImpl @Inject constructor( private val pagingSourceFactory: RecipePagingSourceFactory, private val dataSource: RecipeDataSource, private val logger: Logger, + private val modelMapper: ModelMapper, ) : RecipeRepo { override fun createPager(): Pager { @@ -45,13 +47,21 @@ class RecipeRepoImpl @Inject constructor( override suspend fun refreshRecipeInfo(recipeSlug: String): Result { logger.v { "refreshRecipeInfo() called with: recipeSlug = $recipeSlug" } return runCatchingExceptCancel { - storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug)) + val info = dataSource.requestRecipeInfo(recipeSlug) + val entity = modelMapper.toRecipeEntity(info) + val ingredients = info.recipeIngredients.map { + modelMapper.toRecipeIngredientEntity(it, entity.remoteId) + } + val instructions = info.recipeInstructions.map { + modelMapper.toRecipeInstructionEntity(it, entity.remoteId) + } + storage.saveRecipeInfo(entity, ingredients, instructions) }.onFailure { logger.e(it) { "loadRecipeInfo: can't update full recipe info" } } } - override suspend fun loadRecipeInfo(recipeId: String): FullRecipeEntity? { + override suspend fun loadRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions? { logger.v { "loadRecipeInfo() called with: recipeId = $recipeId" } val recipeInfo = storage.queryRecipeInfo(recipeId) logger.v { "loadRecipeInfo() returned: $recipeInfo" } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt index 1cd8d56..26921f2 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt @@ -5,12 +5,12 @@ import androidx.paging.* import androidx.paging.LoadType.PREPEND import androidx.paging.LoadType.REFRESH import gq.kirmanak.mealient.architecture.configuration.AppDispatchers -import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource +import gq.kirmanak.mealient.database.recipe.RecipeStorage import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.datasource.runCatchingExceptCancel -import gq.kirmanak.mealient.extensions.toRecipeSummaryEntity import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.model_mapper.ModelMapper import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext @@ -24,6 +24,7 @@ class RecipesRemoteMediator @Inject constructor( private val network: RecipeDataSource, private val pagingSourceFactory: RecipePagingSourceFactory, private val logger: Logger, + private val modelMapper: ModelMapper, private val dispatchers: AppDispatchers, ) : RemoteMediator() { @@ -75,7 +76,7 @@ class RecipesRemoteMediator @Inject constructor( val entities = withContext(dispatchers.default) { recipes.map { recipe -> val isFavorite = favorites.contains(recipe.slug) - recipe.toRecipeSummaryEntity(isFavorite) + modelMapper.toRecipeSummaryEntity(recipe, isFavorite) } } if (loadType == REFRESH) storage.refreshAll(entities) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt index 5f9b2ce..4df3074 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt @@ -1,5 +1,8 @@ package gq.kirmanak.mealient.data.recipes.network +import gq.kirmanak.mealient.datasource.models.FullRecipeInfo +import gq.kirmanak.mealient.datasource.models.RecipeSummaryInfo + interface RecipeDataSource { suspend fun requestRecipes(start: Int, limit: Int): List diff --git a/app/src/main/java/gq/kirmanak/mealient/data/share/ParseRecipeDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/share/ParseRecipeDataSource.kt index 27c3def..4369b0b 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/share/ParseRecipeDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/share/ParseRecipeDataSource.kt @@ -1,5 +1,7 @@ package gq.kirmanak.mealient.data.share +import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo + interface ParseRecipeDataSource { suspend fun parseRecipeFromURL(parseRecipeURLInfo: ParseRecipeURLInfo): String diff --git a/app/src/main/java/gq/kirmanak/mealient/data/share/ShareRecipeRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/share/ShareRecipeRepoImpl.kt index 7d711ae..a36242f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/share/ShareRecipeRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/share/ShareRecipeRepoImpl.kt @@ -1,6 +1,7 @@ package gq.kirmanak.mealient.data.share import androidx.core.util.PatternsCompat +import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt index be8828e..7991bd4 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt @@ -15,6 +15,7 @@ import gq.kirmanak.mealient.data.auth.impl.AuthDataSourceImpl import gq.kirmanak.mealient.data.auth.impl.AuthRepoImpl import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl import gq.kirmanak.mealient.datasource.AuthenticationProvider +import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo import javax.inject.Singleton @Module @@ -45,4 +46,8 @@ interface AuthModule { @Binds @Singleton fun bindAuthStorage(authStorageImpl: AuthStorageImpl): AuthStorage + + @Binds + @Singleton + fun bindShoppingListsAuthRepo(impl: AuthRepoImpl): ShoppingListsAuthRepo } diff --git a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt index 210b3de..fcbeb58 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt @@ -10,8 +10,6 @@ import dagger.hilt.components.SingletonComponent import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper import gq.kirmanak.mealient.data.recipes.RecipeRepo -import gq.kirmanak.mealient.data.recipes.db.RecipeStorage -import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl import gq.kirmanak.mealient.data.recipes.impl.* import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity @@ -27,10 +25,6 @@ interface RecipeModule { @Singleton fun provideRecipeDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): RecipeDataSource - @Binds - @Singleton - fun provideRecipeStorage(recipeStorageImpl: RecipeStorageImpl): RecipeStorage - @Binds @Singleton fun provideRecipeRepo(recipeRepoImpl: RecipeRepoImpl): RecipeRepo diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/ModelMappings.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/ModelMappings.kt deleted file mode 100644 index 13eed03..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/ModelMappings.kt +++ /dev/null @@ -1,224 +0,0 @@ -package gq.kirmanak.mealient.extensions - -import gq.kirmanak.mealient.data.add.AddRecipeInfo -import gq.kirmanak.mealient.data.add.AddRecipeIngredientInfo -import gq.kirmanak.mealient.data.add.AddRecipeInstructionInfo -import gq.kirmanak.mealient.data.add.AddRecipeSettingsInfo -import gq.kirmanak.mealient.data.baseurl.VersionInfo -import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo -import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo -import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo -import gq.kirmanak.mealient.data.recipes.network.RecipeSettingsInfo -import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo -import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo -import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity -import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity -import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity -import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity -import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0 -import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0 -import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0 -import gq.kirmanak.mealient.datasource.v0.models.AddRecipeSettingsV0 -import gq.kirmanak.mealient.datasource.v0.models.GetRecipeIngredientResponseV0 -import gq.kirmanak.mealient.datasource.v0.models.GetRecipeInstructionResponseV0 -import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0 -import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0 -import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0 -import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0 -import gq.kirmanak.mealient.datasource.v1.models.AddRecipeIngredientV1 -import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1 -import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1 -import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 -import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1 -import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1 -import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 -import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSettingsResponseV1 -import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 -import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1 -import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 -import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1 -import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft -import java.util.* - -fun FullRecipeInfo.toRecipeEntity() = RecipeEntity( - remoteId = remoteId, - recipeYield = recipeYield, - disableAmounts = settings.disableAmounts, -) - -fun RecipeIngredientInfo.toRecipeIngredientEntity(remoteId: String) = RecipeIngredientEntity( - recipeId = remoteId, - note = note, - unit = unit, - food = food, - quantity = quantity, - title = title, -) - -fun RecipeInstructionInfo.toRecipeInstructionEntity(remoteId: String) = RecipeInstructionEntity( - recipeId = remoteId, - text = text -) - -fun GetRecipeSummaryResponseV0.toRecipeSummaryInfo() = RecipeSummaryInfo( - remoteId = remoteId.toString(), - name = name, - slug = slug, - description = description, - dateAdded = dateAdded, - dateUpdated = dateUpdated, - imageId = slug, -) - -fun GetRecipeSummaryResponseV1.toRecipeSummaryInfo() = RecipeSummaryInfo( - remoteId = remoteId, - name = name, - slug = slug, - description = description, - dateAdded = dateAdded, - dateUpdated = dateUpdated, - imageId = remoteId, -) - -fun RecipeSummaryInfo.toRecipeSummaryEntity(isFavorite: Boolean) = RecipeSummaryEntity( - remoteId = remoteId, - name = name, - slug = slug, - description = description, - dateAdded = dateAdded, - dateUpdated = dateUpdated, - imageId = imageId, - isFavorite = isFavorite, -) - -fun VersionResponseV0.toVersionInfo() = VersionInfo(version) - -fun VersionResponseV1.toVersionInfo() = VersionInfo(version) - -fun AddRecipeDraft.toAddRecipeInfo() = AddRecipeInfo( - name = recipeName, - description = recipeDescription, - recipeYield = recipeYield, - recipeIngredient = recipeIngredients.map { AddRecipeIngredientInfo(note = it) }, - recipeInstructions = recipeInstructions.map { AddRecipeInstructionInfo(text = it) }, - settings = AddRecipeSettingsInfo( - public = isRecipePublic, - disableComments = areCommentsDisabled, - ) -) - -fun AddRecipeInfo.toDraft(): AddRecipeDraft = AddRecipeDraft( - recipeName = name, - recipeDescription = description, - recipeYield = recipeYield, - recipeInstructions = recipeInstructions.map { it.text }, - recipeIngredients = recipeIngredient.map { it.note }, - isRecipePublic = settings.public, - areCommentsDisabled = settings.disableComments, -) - -fun GetRecipeResponseV0.toFullRecipeInfo() = FullRecipeInfo( - remoteId = remoteId.toString(), - name = name, - recipeYield = recipeYield, - recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() }, - recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() }, - settings = RecipeSettingsInfo(disableAmounts = true) -) - -fun GetRecipeIngredientResponseV0.toRecipeIngredientInfo() = RecipeIngredientInfo( - note = note, - unit = null, - food = null, - quantity = 1.0, - title = null, -) - -fun GetRecipeInstructionResponseV0.toRecipeInstructionInfo() = RecipeInstructionInfo( - text = text -) - -fun GetRecipeResponseV1.toFullRecipeInfo() = FullRecipeInfo( - remoteId = remoteId, - name = name, - recipeYield = recipeYield, - recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() }, - recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() }, - settings = settings.toRecipeSettingsInfo(), -) - -private fun GetRecipeSettingsResponseV1.toRecipeSettingsInfo() = RecipeSettingsInfo( - disableAmounts = disableAmount, -) - -fun GetRecipeIngredientResponseV1.toRecipeIngredientInfo() = RecipeIngredientInfo( - note = note, - unit = unit?.name, - food = food?.name, - quantity = quantity, - title = title, -) - -fun GetRecipeInstructionResponseV1.toRecipeInstructionInfo() = RecipeInstructionInfo( - text = text -) - -fun AddRecipeInfo.toV0Request() = AddRecipeRequestV0( - name = name, - description = description, - recipeYield = recipeYield, - recipeIngredient = recipeIngredient.map { it.toV0Ingredient() }, - recipeInstructions = recipeInstructions.map { it.toV0Instruction() }, - settings = settings.toV0Settings(), -) - -private fun AddRecipeSettingsInfo.toV0Settings() = AddRecipeSettingsV0( - disableComments = disableComments, - public = public, -) - -private fun AddRecipeIngredientInfo.toV0Ingredient() = AddRecipeIngredientV0( - note = note, -) - -private fun AddRecipeInstructionInfo.toV0Instruction() = AddRecipeInstructionV0( - text = text, -) - - -fun AddRecipeInfo.toV1CreateRequest() = CreateRecipeRequestV1( - name = name, -) - -fun AddRecipeInfo.toV1UpdateRequest() = UpdateRecipeRequestV1( - description = description, - recipeYield = recipeYield, - recipeIngredient = recipeIngredient.map { it.toV1Ingredient() }, - recipeInstructions = recipeInstructions.map { it.toV1Instruction() }, - settings = settings.toV1Settings(), -) - -private fun AddRecipeSettingsInfo.toV1Settings() = AddRecipeSettingsV1( - disableComments = disableComments, - public = public, -) - -private fun AddRecipeIngredientInfo.toV1Ingredient() = AddRecipeIngredientV1( - id = UUID.randomUUID().toString(), - note = note, -) - -private fun AddRecipeInstructionInfo.toV1Instruction() = AddRecipeInstructionV1( - id = UUID.randomUUID().toString(), - text = text, - ingredientReferences = emptyList(), -) - -fun ParseRecipeURLInfo.toV1Request() = ParseRecipeURLRequestV1( - url = url, - includeTags = includeTags, -) - -fun ParseRecipeURLInfo.toV0Request() = ParseRecipeURLRequestV0( - url = url, -) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt index af6c010..95738a8 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt @@ -15,11 +15,14 @@ import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAddRecipeFr import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalAuthenticationFragment import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalBaseURLFragment import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalRecipesListFragment +import gq.kirmanak.mealient.NavGraphDirections.Companion.actionGlobalShoppingListsFragment import gq.kirmanak.mealient.R import gq.kirmanak.mealient.databinding.MainActivityBinding import gq.kirmanak.mealient.extensions.collectWhenResumed import gq.kirmanak.mealient.extensions.observeOnce +import gq.kirmanak.mealient.ui.ActivityUiState import gq.kirmanak.mealient.ui.BaseActivity +import gq.kirmanak.mealient.ui.CheckableMenuItem @AndroidEntryPoint class MainActivity : BaseActivity( @@ -60,7 +63,7 @@ class MainActivity : BaseActivity( viewModel.onSearchQuery(query.trim().takeUnless { it.isEmpty() }) } binding.navigationView.setNavigationItemSelectedListener(::onNavigationItemSelected) - viewModel.uiStateLive.observe(this, ::onUiStateChange) + collectWhenResumed(viewModel.uiState, ::onUiStateChange) collectWhenResumed(viewModel.clearSearchViewFocus) { logger.d { "clearSearchViewFocus(): received event" } binding.toolbar.clearSearchFocus() @@ -77,6 +80,7 @@ class MainActivity : BaseActivity( val directions = when (menuItem.itemId) { R.id.add_recipe -> actionGlobalAddRecipeFragment() R.id.recipes_list -> actionGlobalRecipesListFragment() + R.id.shopping_lists -> actionGlobalShoppingListsFragment() R.id.change_url -> actionGlobalBaseURLFragment(false) R.id.login -> actionGlobalAuthenticationFragment() R.id.logout -> { @@ -90,15 +94,24 @@ class MainActivity : BaseActivity( return true } - private fun onUiStateChange(uiState: MainActivityUiState) { + private fun onUiStateChange(uiState: ActivityUiState) { logger.v { "onUiStateChange() called with: uiState = $uiState" } + val checkedMenuItem = when (uiState.checkedMenuItem) { + CheckableMenuItem.ShoppingLists -> R.id.shopping_lists + CheckableMenuItem.RecipesList -> R.id.recipes_list + CheckableMenuItem.AddRecipe -> R.id.add_recipe + CheckableMenuItem.ChangeUrl -> R.id.change_url + CheckableMenuItem.Login -> R.id.login + null -> null + } for (menuItem in binding.navigationView.menu.iterator()) { val itemId = menuItem.itemId when (itemId) { R.id.logout -> menuItem.isVisible = uiState.canShowLogout R.id.login -> menuItem.isVisible = uiState.canShowLogin + R.id.shopping_lists -> menuItem.isVisible = uiState.v1MenuItemsVisible } - menuItem.isChecked = itemId == uiState.checkedMenuItemId + menuItem.isChecked = itemId == checkedMenuItem } binding.toolbar.isVisible = uiState.navigationVisible diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt deleted file mode 100644 index eda6f14..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityUiState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package gq.kirmanak.mealient.ui.activity - -import androidx.annotation.IdRes - -data class MainActivityUiState( - val isAuthorized: Boolean = false, - val navigationVisible: Boolean = false, - val searchVisible: Boolean = false, - @IdRes val checkedMenuItemId: Int? = null, -) { - val canShowLogin: Boolean get() = !isAuthorized - - val canShowLogout: Boolean get() = isAuthorized -} diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt index 6fd28a7..9309e9d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt @@ -1,16 +1,23 @@ package gq.kirmanak.mealient.ui.activity -import androidx.lifecycle.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo +import gq.kirmanak.mealient.data.baseurl.ServerVersion import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.ui.ActivityUiState +import gq.kirmanak.mealient.ui.ActivityUiStateController import gq.kirmanak.mealient.ui.baseurl.BaseURLFragmentArgs import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow @@ -24,14 +31,10 @@ class MainActivityViewModel @Inject constructor( private val disclaimerStorage: DisclaimerStorage, private val serverInfoRepo: ServerInfoRepo, private val recipeRepo: RecipeRepo, + private val activityUiStateController: ActivityUiStateController, ) : ViewModel() { - private val _uiState = MutableLiveData(MainActivityUiState()) - val uiStateLive: LiveData - get() = _uiState.distinctUntilChanged() - var uiState: MainActivityUiState - get() = checkNotNull(_uiState.value) { "UiState must not be null" } - private set(value) = _uiState.postValue(value) + val uiState: StateFlow = activityUiStateController.getUiStateFlow() private val _startDestination = MutableLiveData() val startDestination: LiveData = _startDestination @@ -44,6 +47,10 @@ class MainActivityViewModel @Inject constructor( .onEach { isAuthorized -> updateUiState { it.copy(isAuthorized = isAuthorized) } } .launchIn(viewModelScope) + serverInfoRepo.versionUpdates() + .onEach { version -> updateUiState { it.copy(v1MenuItemsVisible = version == ServerVersion.V1) } } + .launchIn(viewModelScope) + viewModelScope.launch { _startDestination.value = when { !disclaimerStorage.isDisclaimerAccepted() -> { @@ -59,8 +66,8 @@ class MainActivityViewModel @Inject constructor( } } - fun updateUiState(updater: (MainActivityUiState) -> MainActivityUiState) { - uiState = updater(uiState) + fun updateUiState(updater: (ActivityUiState) -> ActivityUiState) { + activityUiStateController.updateUiState(updater) } fun logout() { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt index c9cfeb0..7861462 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt @@ -12,15 +12,16 @@ import androidx.fragment.app.viewModels import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R -import gq.kirmanak.mealient.data.add.AddRecipeInfo -import gq.kirmanak.mealient.data.add.AddRecipeIngredientInfo -import gq.kirmanak.mealient.data.add.AddRecipeInstructionInfo -import gq.kirmanak.mealient.data.add.AddRecipeSettingsInfo import gq.kirmanak.mealient.databinding.FragmentAddRecipeBinding import gq.kirmanak.mealient.databinding.ViewSingleInputBinding +import gq.kirmanak.mealient.datasource.models.AddRecipeInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeIngredientInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeInstructionInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeSettingsInfo import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.collectWhenViewResumed import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.ui.CheckableMenuItem import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import javax.inject.Inject @@ -41,7 +42,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) { it.copy( navigationVisible = true, searchVisible = false, - checkedMenuItemId = R.id.add_recipe, + checkedMenuItem = CheckableMenuItem.AddRecipe, ) } viewModel.loadPreservedRequest() diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt index 982e56f..3890df7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt @@ -3,8 +3,8 @@ package gq.kirmanak.mealient.ui.add import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import gq.kirmanak.mealient.data.add.AddRecipeInfo import gq.kirmanak.mealient.data.add.AddRecipeRepo +import gq.kirmanak.mealient.datasource.models.AddRecipeInfo import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.channels.Channel diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt index f28ae2d..4928d1b 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt @@ -13,6 +13,7 @@ import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding import gq.kirmanak.mealient.datasource.NetworkError import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.ui.CheckableMenuItem import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import javax.inject.Inject @@ -32,7 +33,11 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } binding.button.setOnClickListener { onLoginClicked() } activityViewModel.updateUiState { - it.copy(navigationVisible = true, searchVisible = false, checkedMenuItemId = R.id.login) + it.copy( + navigationVisible = true, + searchVisible = false, + checkedMenuItem = CheckableMenuItem.AddRecipe + ) } viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt index 5313d56..0b51028 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt @@ -14,6 +14,7 @@ import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding import gq.kirmanak.mealient.datasource.NetworkError import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.ui.CheckableMenuItem import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.baseurl.BaseURLFragmentDirections.Companion.actionBaseURLFragmentToRecipesListFragment @@ -39,7 +40,7 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) { it.copy( navigationVisible = !args.isOnboarding, searchVisible = false, - checkedMenuItemId = R.id.change_url, + checkedMenuItem = CheckableMenuItem.ChangeUrl, ) } } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt index 015e659..92c01f7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt @@ -10,6 +10,7 @@ import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.datasource.NetworkError import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo import gq.kirmanak.mealient.ui.OperationUiState import kotlinx.coroutines.launch import javax.inject.Inject @@ -20,6 +21,7 @@ class BaseURLViewModel @Inject constructor( private val authRepo: AuthRepo, private val recipeRepo: RecipeRepo, private val logger: Logger, + private val shoppingListsRepo: ShoppingListsRepo, ) : ViewModel() { private val _uiState = MutableLiveData>(OperationUiState.Initial()) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt index d7db583..87ad600 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt @@ -58,7 +58,11 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) { } viewModel.startCountDown() activityViewModel.updateUiState { - it.copy(navigationVisible = false, searchVisible = false, checkedMenuItemId = null) + it.copy( + navigationVisible = false, + searchVisible = false, + checkedMenuItem = null + ) } } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt index 1003b26..9db8c14 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListFragment.kt @@ -17,11 +17,13 @@ import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R +import gq.kirmanak.mealient.architecture.valueUpdatesOnly import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.databinding.FragmentRecipesListBinding import gq.kirmanak.mealient.datasource.NetworkError import gq.kirmanak.mealient.extensions.* import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.ui.CheckableMenuItem import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.recipes.RecipesListFragmentDirections.Companion.actionRecipesFragmentToRecipeInfoFragment import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactory @@ -54,7 +56,7 @@ class RecipesListFragment : Fragment(R.layout.fragment_recipes_list) { it.copy( navigationVisible = true, searchVisible = true, - checkedMenuItemId = R.id.recipes_list + checkedMenuItem = CheckableMenuItem.RecipesList, ) } collectWhenViewResumed(viewModel.showFavoriteIcon) { showFavoriteIcon -> diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt index f9ddddf..092bcea 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt @@ -7,10 +7,10 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import dagger.hilt.android.lifecycle.HiltViewModel +import gq.kirmanak.mealient.architecture.valueUpdatesOnly import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity -import gq.kirmanak.mealient.extensions.valueUpdatesOnly import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/res/menu/navigation_menu.xml b/app/src/main/res/menu/navigation_menu.xml index e896a64..aa02f1e 100644 --- a/app/src/main/res/menu/navigation_menu.xml +++ b/app/src/main/res/menu/navigation_menu.xml @@ -12,6 +12,13 @@ android:icon="@drawable/ic_add" android:title="@string/menu_navigation_drawer_add_recipe" /> + + + + + @@ -81,4 +86,9 @@ android:id="@+id/action_global_baseURLFragment" app:destination="@id/baseURLFragment" app:popUpTo="@id/recipesListFragment" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 7c57d74..716d922 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -65,4 +65,5 @@ Удалить рецепт %1$s добавлено в избранное %1$s удалено из избранного + Списки покупок diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e805c47..62853ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -68,4 +68,5 @@ Delete recipe Added %1$s to favorites Removed %1$s from favorites + Shopping lists \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoTest.kt index bbcb139..5eaaa7d 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoTest.kt @@ -3,10 +3,12 @@ package gq.kirmanak.mealient.data.add.impl import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.add.AddRecipeDataSource import gq.kirmanak.mealient.data.add.AddRecipeRepo +import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_INFO import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage +import gq.kirmanak.mealient.datastore_test.PORRIDGE_RECIPE_DRAFT +import gq.kirmanak.mealient.model_mapper.ModelMapper +import gq.kirmanak.mealient.model_mapper.ModelMapperImpl import gq.kirmanak.mealient.test.BaseUnitTest -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_INFO -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_DRAFT import io.mockk.* import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -24,12 +26,14 @@ class AddRecipeRepoTest : BaseUnitTest() { @MockK(relaxUnitFun = true) lateinit var storage: AddRecipeStorage + private val modelMapper: ModelMapper = ModelMapperImpl() + private lateinit var subject: AddRecipeRepo @Before override fun setUp() { super.setUp() - subject = AddRecipeRepoImpl(dataSource, storage, logger) + subject = AddRecipeRepoImpl(dataSource, storage, logger, modelMapper) } @Test diff --git a/app/src/test/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoTest.kt index 91763eb..07cf0e3 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoTest.kt @@ -1,10 +1,11 @@ package gq.kirmanak.mealient.data.baseurl import com.google.common.truth.Truth.assertThat +import gq.kirmanak.mealient.datasource.models.VersionInfo +import gq.kirmanak.mealient.datasource_test.VERSION_INFO_V0 import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_VERSION import gq.kirmanak.mealient.test.BaseUnitTest -import gq.kirmanak.mealient.test.RecipeImplTestData.VERSION_INFO_V0 import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK diff --git a/app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt index d1de8af..a320678 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt @@ -5,6 +5,18 @@ import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_INFO +import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_REQUEST_V0 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_CREATE_RECIPE_REQUEST_V1 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_FULL_RECIPE_INFO +import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_RESPONSE_V1 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_UPDATE_RECIPE_REQUEST_V1 +import gq.kirmanak.mealient.datasource_test.RECIPE_SUMMARY_PORRIDGE_V0 +import gq.kirmanak.mealient.datasource_test.RECIPE_SUMMARY_PORRIDGE_V1 +import gq.kirmanak.mealient.model_mapper.ModelMapper +import gq.kirmanak.mealient.model_mapper.ModelMapperImpl import gq.kirmanak.mealient.test.AuthImplTestData.FAVORITE_RECIPES_LIST import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER import gq.kirmanak.mealient.test.AuthImplTestData.TEST_SERVER_VERSION_V0 @@ -12,16 +24,6 @@ import gq.kirmanak.mealient.test.AuthImplTestData.TEST_SERVER_VERSION_V1 import gq.kirmanak.mealient.test.AuthImplTestData.USER_INFO_V0 import gq.kirmanak.mealient.test.AuthImplTestData.USER_INFO_V1 import gq.kirmanak.mealient.test.BaseUnitTest -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_INFO -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_REQUEST_V0 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_CREATE_RECIPE_REQUEST_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_FULL_RECIPE_INFO -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_RESPONSE_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_UPDATE_RECIPE_REQUEST_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.RECIPE_SUMMARY_PORRIDGE_V0 -import gq.kirmanak.mealient.test.RecipeImplTestData.RECIPE_SUMMARY_PORRIDGE_V1 import io.mockk.* import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,12 +47,14 @@ class MealieDataSourceWrapperTest : BaseUnitTest() { @MockK(relaxUnitFun = true) lateinit var v1Source: MealieDataSourceV1 + private val modelMapper: ModelMapper = ModelMapperImpl() + lateinit var subject: MealieDataSourceWrapper @Before override fun setUp() { super.setUp() - subject = MealieDataSourceWrapper(serverInfoRepo, v0Source, v1Source) + subject = MealieDataSourceWrapper(serverInfoRepo, v0Source, v1Source, modelMapper) coEvery { v0Source.requestUserInfo() } returns USER_INFO_V0 coEvery { v1Source.requestUserInfo() } returns USER_INFO_V1 } diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImplTest.kt deleted file mode 100644 index 6cfb6f0..0000000 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImplTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -package gq.kirmanak.mealient.data.recipes.db - -import com.google.common.truth.Truth.assertThat -import dagger.hilt.android.testing.HiltAndroidTest -import gq.kirmanak.mealient.database.AppDb -import gq.kirmanak.mealient.test.HiltRobolectricTest -import gq.kirmanak.mealient.test.RecipeImplTestData.BREAD_INGREDIENT -import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_BREAD_RECIPE_INGREDIENT_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_FULL_RECIPE_INFO -import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_PORRIDGE_INFO_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_CAKE_RECIPE_INSTRUCTION_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_INSTRUCTION -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_FULL_RECIPE_INFO -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARY_ENTITIES -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Test -import javax.inject.Inject - -@HiltAndroidTest -@OptIn(ExperimentalCoroutinesApi::class) -class RecipeStorageImplTest : HiltRobolectricTest() { - - @Inject - lateinit var subject: RecipeStorageImpl - - @Inject - lateinit var appDb: AppDb - - @Test - fun `when saveRecipes then saves recipes`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES) - val actualTags = appDb.recipeDao().queryAllRecipes() - assertThat(actualTags).containsExactly( - CAKE_RECIPE_SUMMARY_ENTITY, - PORRIDGE_RECIPE_SUMMARY_ENTITY - ) - } - - @Test - fun `when refreshAll then old recipes aren't preserved`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES) - subject.refreshAll(listOf(CAKE_RECIPE_SUMMARY_ENTITY)) - val actual = appDb.recipeDao().queryAllRecipes() - assertThat(actual).containsExactly(CAKE_RECIPE_SUMMARY_ENTITY) - } - - @Test - fun `when clearAllLocalData then recipes aren't preserved`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES) - subject.clearAllLocalData() - val actual = appDb.recipeDao().queryAllRecipes() - assertThat(actual).isEmpty() - } - - @Test - fun `when saveRecipeInfo then saves recipe info`() = runTest { - subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY)) - subject.saveRecipeInfo(CAKE_FULL_RECIPE_INFO) - val actual = appDb.recipeDao().queryFullRecipeInfo("1") - assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY) - } - - @Test - fun `when saveRecipeInfo with two then saves second`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES) - subject.saveRecipeInfo(CAKE_FULL_RECIPE_INFO) - subject.saveRecipeInfo(PORRIDGE_FULL_RECIPE_INFO) - val actual = appDb.recipeDao().queryFullRecipeInfo("2") - assertThat(actual).isEqualTo(FULL_PORRIDGE_INFO_ENTITY) - } - - @Test - fun `when saveRecipeInfo secondly then overwrites ingredients`() = runTest { - subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY)) - subject.saveRecipeInfo(CAKE_FULL_RECIPE_INFO) - val newRecipe = CAKE_FULL_RECIPE_INFO.copy(recipeIngredients = listOf(BREAD_INGREDIENT)) - subject.saveRecipeInfo(newRecipe) - val actual = appDb.recipeDao().queryFullRecipeInfo("1")?.recipeIngredients - val expected = listOf(CAKE_BREAD_RECIPE_INGREDIENT_ENTITY.copy(localId = 3)) - assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when saveRecipeInfo secondly then overwrites instructions`() = runTest { - subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY)) - subject.saveRecipeInfo(CAKE_FULL_RECIPE_INFO) - val newRecipe = CAKE_FULL_RECIPE_INFO.copy(recipeInstructions = listOf(MIX_INSTRUCTION)) - subject.saveRecipeInfo(newRecipe) - val actual = appDb.recipeDao().queryFullRecipeInfo("1")?.recipeInstructions - val expected = listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY.copy(localId = 3)) - assertThat(actual).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImplTest.kt index e5374aa..c5f3b13 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipePagingSourceFactoryImplTest.kt @@ -3,12 +3,12 @@ package gq.kirmanak.mealient.data.recipes.impl import androidx.paging.PagingSource import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidTest -import gq.kirmanak.mealient.data.recipes.db.RecipeStorage +import gq.kirmanak.mealient.database.CAKE_RECIPE_SUMMARY_ENTITY +import gq.kirmanak.mealient.database.PORRIDGE_RECIPE_SUMMARY_ENTITY +import gq.kirmanak.mealient.database.TEST_RECIPE_SUMMARY_ENTITIES +import gq.kirmanak.mealient.database.recipe.RecipeStorage import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.test.HiltRobolectricTest -import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARY_ENTITIES import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoTest.kt index 2a4fb81..165b1cc 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoTest.kt @@ -3,13 +3,20 @@ package gq.kirmanak.mealient.data.recipes.impl import androidx.paging.LoadType import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.recipes.RecipeRepo -import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource +import gq.kirmanak.mealient.database.BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY +import gq.kirmanak.mealient.database.CAKE_BREAD_RECIPE_INGREDIENT_ENTITY +import gq.kirmanak.mealient.database.CAKE_RECIPE_ENTITY +import gq.kirmanak.mealient.database.CAKE_RECIPE_SUMMARY_ENTITY +import gq.kirmanak.mealient.database.CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY +import gq.kirmanak.mealient.database.FULL_CAKE_INFO_ENTITY +import gq.kirmanak.mealient.database.MIX_CAKE_RECIPE_INSTRUCTION_ENTITY +import gq.kirmanak.mealient.database.recipe.RecipeStorage import gq.kirmanak.mealient.datasource.NetworkError.Unauthorized +import gq.kirmanak.mealient.datasource_test.CAKE_FULL_RECIPE_INFO +import gq.kirmanak.mealient.model_mapper.ModelMapper +import gq.kirmanak.mealient.model_mapper.ModelMapperImpl import gq.kirmanak.mealient.test.BaseUnitTest -import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_FULL_RECIPE_INFO -import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY import io.mockk.coEvery import io.mockk.coVerify import io.mockk.coVerifyOrder @@ -36,12 +43,21 @@ class RecipeRepoTest : BaseUnitTest() { @MockK(relaxUnitFun = true) lateinit var pagingSourceFactory: RecipePagingSourceFactory + private val modelMapper: ModelMapper = ModelMapperImpl() + lateinit var subject: RecipeRepo @Before override fun setUp() { super.setUp() - subject = RecipeRepoImpl(remoteMediator, storage, pagingSourceFactory, dataSource, logger) + subject = RecipeRepoImpl( + remoteMediator, + storage, + pagingSourceFactory, + dataSource, + logger, + modelMapper, + ) } @Test @@ -55,7 +71,18 @@ class RecipeRepoTest : BaseUnitTest() { fun `when refreshRecipeInfo expect call to storage`() = runTest { coEvery { dataSource.requestRecipeInfo(eq("cake")) } returns CAKE_FULL_RECIPE_INFO subject.refreshRecipeInfo("cake") - coVerify { storage.saveRecipeInfo(eq(CAKE_FULL_RECIPE_INFO)) } + coVerify { + storage.saveRecipeInfo( + eq(CAKE_RECIPE_ENTITY), + eq( + listOf( + CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, + CAKE_BREAD_RECIPE_INGREDIENT_ENTITY + ) + ), + eq(listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY)) + ) + } } @Test diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt index a85475f..4961520 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt @@ -3,13 +3,15 @@ package gq.kirmanak.mealient.data.recipes.impl import androidx.paging.* import androidx.paging.LoadType.* import com.google.common.truth.Truth.assertThat -import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource +import gq.kirmanak.mealient.database.TEST_RECIPE_SUMMARY_ENTITIES +import gq.kirmanak.mealient.database.recipe.RecipeStorage import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.datasource.NetworkError.Unauthorized +import gq.kirmanak.mealient.datasource_test.TEST_RECIPE_SUMMARIES +import gq.kirmanak.mealient.model_mapper.ModelMapper +import gq.kirmanak.mealient.model_mapper.ModelMapperImpl import gq.kirmanak.mealient.test.BaseUnitTest -import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARIES -import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARY_ENTITIES import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK @@ -41,6 +43,8 @@ class RecipesRemoteMediatorTest : BaseUnitTest() { @MockK(relaxUnitFun = true) lateinit var pagingSourceFactory: RecipePagingSourceFactory + private val modelMapper: ModelMapper = ModelMapperImpl() + @Before override fun setUp() { super.setUp() @@ -50,6 +54,7 @@ class RecipesRemoteMediatorTest : BaseUnitTest() { pagingSourceFactory = pagingSourceFactory, logger = logger, dispatchers = dispatchers, + modelMapper = modelMapper, ) coEvery { dataSource.getFavoriteRecipes() } returns emptyList() } diff --git a/app/src/test/java/gq/kirmanak/mealient/data/share/ShareRecipeRepoImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/share/ShareRecipeRepoImplTest.kt index 1f03463..2427f0e 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/share/ShareRecipeRepoImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/share/ShareRecipeRepoImplTest.kt @@ -1,5 +1,6 @@ package gq.kirmanak.mealient.data.share +import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo import gq.kirmanak.mealient.test.BaseUnitTest import io.mockk.coEvery import io.mockk.coVerify diff --git a/app/src/test/java/gq/kirmanak/mealient/extensions/ModelMappingsTest.kt b/app/src/test/java/gq/kirmanak/mealient/extensions/ModelMappingsTest.kt deleted file mode 100644 index 578af8b..0000000 --- a/app/src/test/java/gq/kirmanak/mealient/extensions/ModelMappingsTest.kt +++ /dev/null @@ -1,146 +0,0 @@ -package gq.kirmanak.mealient.extensions - -import com.google.common.truth.Truth.assertThat -import gq.kirmanak.mealient.test.BaseUnitTest -import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_FULL_RECIPE_INFO -import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.MILK_RECIPE_INGREDIENT_INFO -import gq.kirmanak.mealient.test.RecipeImplTestData.MILK_RECIPE_INGREDIENT_RESPONSE_V0 -import gq.kirmanak.mealient.test.RecipeImplTestData.MILK_RECIPE_INGREDIENT_RESPONSE_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_CAKE_RECIPE_INSTRUCTION_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_INSTRUCTION -import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_RECIPE_INSTRUCTION_INFO -import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_RECIPE_INSTRUCTION_RESPONSE_V0 -import gq.kirmanak.mealient.test.RecipeImplTestData.MIX_RECIPE_INSTRUCTION_RESPONSE_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_INFO -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_REQUEST_V0 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_CREATE_RECIPE_REQUEST_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_FULL_RECIPE_INFO -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_DRAFT -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_RESPONSE_V0 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_RESPONSE_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_ENTITY -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_UPDATE_RECIPE_REQUEST_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.RECIPE_SUMMARY_PORRIDGE_V0 -import gq.kirmanak.mealient.test.RecipeImplTestData.RECIPE_SUMMARY_PORRIDGE_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.SUGAR_INGREDIENT -import gq.kirmanak.mealient.test.RecipeImplTestData.VERSION_INFO_V0 -import gq.kirmanak.mealient.test.RecipeImplTestData.VERSION_INFO_V1 -import gq.kirmanak.mealient.test.RecipeImplTestData.VERSION_RESPONSE_V0 -import gq.kirmanak.mealient.test.RecipeImplTestData.VERSION_RESPONSE_V1 -import org.junit.Test - -class ModelMappingsTest : BaseUnitTest() { - - @Test - fun `when toAddRecipeRequest then fills fields correctly`() { - assertThat(PORRIDGE_RECIPE_DRAFT.toAddRecipeInfo()).isEqualTo(PORRIDGE_ADD_RECIPE_INFO) - } - - @Test - fun `when toDraft then fills fields correctly`() { - assertThat(PORRIDGE_ADD_RECIPE_INFO.toDraft()).isEqualTo(PORRIDGE_RECIPE_DRAFT) - } - - @Test - fun `when full recipe info to entity expect correct entity`() { - assertThat(CAKE_FULL_RECIPE_INFO.toRecipeEntity()).isEqualTo(CAKE_RECIPE_ENTITY) - } - - @Test - fun `when ingredient info to entity expect correct entity`() { - val actual = SUGAR_INGREDIENT.toRecipeIngredientEntity("1") - assertThat(actual).isEqualTo(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY) - } - - @Test - fun `when instruction info to entity expect correct entity`() { - val actual = MIX_INSTRUCTION.toRecipeInstructionEntity("1") - assertThat(actual).isEqualTo(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY) - } - - @Test - fun `when summary v0 to info expect correct info`() { - val actual = PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0.toRecipeSummaryInfo() - assertThat(actual).isEqualTo(RECIPE_SUMMARY_PORRIDGE_V0) - } - - @Test - fun `when summary v1 to info expect correct info`() { - val actual = PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1.toRecipeSummaryInfo() - assertThat(actual).isEqualTo(RECIPE_SUMMARY_PORRIDGE_V1) - } - - @Test - fun `when summary info to entity expect correct entity`() { - val actual = RECIPE_SUMMARY_PORRIDGE_V0.toRecipeSummaryEntity(isFavorite = false) - assertThat(actual).isEqualTo(PORRIDGE_RECIPE_SUMMARY_ENTITY) - } - - @Test - fun `when version response v0 to info expect correct info`() { - assertThat(VERSION_RESPONSE_V0.toVersionInfo()).isEqualTo(VERSION_INFO_V0) - } - - @Test - fun `when version response v1 to info expect correct info`() { - assertThat(VERSION_RESPONSE_V1.toVersionInfo()).isEqualTo(VERSION_INFO_V1) - } - - @Test - fun `when recipe ingredient response v0 to info expect correct info`() { - val actual = MILK_RECIPE_INGREDIENT_RESPONSE_V0.toRecipeIngredientInfo() - assertThat(actual).isEqualTo(MILK_RECIPE_INGREDIENT_INFO) - } - - @Test - fun `when recipe ingredient response v1 to info expect correct info`() { - val actual = MILK_RECIPE_INGREDIENT_RESPONSE_V1.toRecipeIngredientInfo() - assertThat(actual).isEqualTo(MILK_RECIPE_INGREDIENT_INFO) - } - - @Test - fun `when recipe instruction response v0 to info expect correct info`() { - val actual = MIX_RECIPE_INSTRUCTION_RESPONSE_V0.toRecipeInstructionInfo() - assertThat(actual).isEqualTo(MIX_RECIPE_INSTRUCTION_INFO) - } - - @Test - fun `when recipe instruction response v1 to info expect correct info`() { - val actual = MIX_RECIPE_INSTRUCTION_RESPONSE_V1.toRecipeInstructionInfo() - assertThat(actual).isEqualTo(MIX_RECIPE_INSTRUCTION_INFO) - } - - @Test - fun `when recipe response v0 to info expect correct info`() { - val actual = PORRIDGE_RECIPE_RESPONSE_V0.toFullRecipeInfo() - assertThat(actual).isEqualTo(PORRIDGE_FULL_RECIPE_INFO) - } - - @Test - fun `when recipe response v1 to info expect correct info`() { - val actual = PORRIDGE_RECIPE_RESPONSE_V1.toFullRecipeInfo() - assertThat(actual).isEqualTo(PORRIDGE_FULL_RECIPE_INFO) - } - - @Test - fun `when add recipe info to request v0 expect correct request`() { - val actual = PORRIDGE_ADD_RECIPE_INFO.toV0Request() - assertThat(actual).isEqualTo(PORRIDGE_ADD_RECIPE_REQUEST_V0) - } - - @Test - fun `when add recipe info to create request v1 expect correct request`() { - val actual = PORRIDGE_ADD_RECIPE_INFO.toV1CreateRequest() - assertThat(actual).isEqualTo(PORRIDGE_CREATE_RECIPE_REQUEST_V1) - } - - @Test - fun `when add recipe info to update request v1 expect correct request`() { - val actual = PORRIDGE_ADD_RECIPE_INFO.toV1UpdateRequest() - assertThat(actual).isEqualTo(PORRIDGE_UPDATE_RECIPE_REQUEST_V1) - } -} \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt b/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt deleted file mode 100644 index a80ff6a..0000000 --- a/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt +++ /dev/null @@ -1,440 +0,0 @@ -package gq.kirmanak.mealient.test - -import gq.kirmanak.mealient.data.add.AddRecipeInfo -import gq.kirmanak.mealient.data.add.AddRecipeIngredientInfo -import gq.kirmanak.mealient.data.add.AddRecipeInstructionInfo -import gq.kirmanak.mealient.data.add.AddRecipeSettingsInfo -import gq.kirmanak.mealient.data.baseurl.VersionInfo -import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo -import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo -import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo -import gq.kirmanak.mealient.data.recipes.network.RecipeSettingsInfo -import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo -import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity -import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity -import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity -import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity -import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity -import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0 -import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0 -import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0 -import gq.kirmanak.mealient.datasource.v0.models.AddRecipeSettingsV0 -import gq.kirmanak.mealient.datasource.v0.models.GetRecipeIngredientResponseV0 -import gq.kirmanak.mealient.datasource.v0.models.GetRecipeInstructionResponseV0 -import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0 -import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0 -import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0 -import gq.kirmanak.mealient.datasource.v1.models.AddRecipeIngredientV1 -import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1 -import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1 -import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 -import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1 -import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1 -import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 -import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSettingsResponseV1 -import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 -import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 -import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1 -import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalDateTime - -object RecipeImplTestData { - val RECIPE_SUMMARY_CAKE = RecipeSummaryInfo( - remoteId = "1", - name = "Cake", - slug = "cake", - description = "A tasty cake", - dateAdded = LocalDate.parse("2021-11-13"), - dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"), - imageId = "cake", - ) - - val RECIPE_SUMMARY_PORRIDGE_V0 = RecipeSummaryInfo( - remoteId = "2", - name = "Porridge", - slug = "porridge", - description = "A tasty porridge", - dateAdded = LocalDate.parse("2021-11-12"), - dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), - imageId = "porridge", - ) - - val RECIPE_SUMMARY_PORRIDGE_V1 = RecipeSummaryInfo( - remoteId = "2", - name = "Porridge", - slug = "porridge", - description = "A tasty porridge", - dateAdded = LocalDate.parse("2021-11-12"), - dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), - imageId = "2", - ) - - val TEST_RECIPE_SUMMARIES = listOf(RECIPE_SUMMARY_CAKE, RECIPE_SUMMARY_PORRIDGE_V0) - - val CAKE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity( - remoteId = "1", - name = "Cake", - slug = "cake", - description = "A tasty cake", - dateAdded = LocalDate.parse("2021-11-13"), - dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"), - imageId = "cake", - isFavorite = false, - ) - - val PORRIDGE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity( - remoteId = "2", - name = "Porridge", - slug = "porridge", - description = "A tasty porridge", - dateAdded = LocalDate.parse("2021-11-12"), - dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), - imageId = "porridge", - isFavorite = false, - ) - - val TEST_RECIPE_SUMMARY_ENTITIES = - listOf(CAKE_RECIPE_SUMMARY_ENTITY, PORRIDGE_RECIPE_SUMMARY_ENTITY) - - val SUGAR_INGREDIENT = RecipeIngredientInfo( - note = "2 oz of white sugar", - quantity = 1.0, - unit = null, - food = null, - title = null, - ) - - val BREAD_INGREDIENT = RecipeIngredientInfo( - note = "2 oz of white bread", - quantity = 1.0, - unit = null, - food = null, - title = null, - ) - - private val MILK_INGREDIENT = RecipeIngredientInfo( - note = "2 oz of white milk", - quantity = 1.0, - unit = null, - food = null, - title = null, - ) - - val MIX_INSTRUCTION = RecipeInstructionInfo( - text = "Mix the ingredients" - ) - - private val BAKE_INSTRUCTION = RecipeInstructionInfo( - text = "Bake the ingredients" - ) - - private val BOIL_INSTRUCTION = RecipeInstructionInfo( - text = "Boil the ingredients" - ) - - val CAKE_FULL_RECIPE_INFO = FullRecipeInfo( - remoteId = "1", - name = "Cake", - recipeYield = "4 servings", - recipeIngredients = listOf(SUGAR_INGREDIENT, BREAD_INGREDIENT), - recipeInstructions = listOf(MIX_INSTRUCTION, BAKE_INSTRUCTION), - settings = RecipeSettingsInfo(disableAmounts = true) - ) - - val PORRIDGE_FULL_RECIPE_INFO = FullRecipeInfo( - remoteId = "2", - name = "Porridge", - recipeYield = "3 servings", - recipeIngredients = listOf(SUGAR_INGREDIENT, MILK_INGREDIENT), - recipeInstructions = listOf(MIX_INSTRUCTION, BOIL_INSTRUCTION), - settings = RecipeSettingsInfo(disableAmounts = true) - ) - - val MIX_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( - recipeId = "1", - text = "Mix the ingredients", - ) - - private val BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( - recipeId = "1", - text = "Bake the ingredients", - ) - - val CAKE_RECIPE_ENTITY = RecipeEntity( - remoteId = "1", - recipeYield = "4 servings", - disableAmounts = true, - ) - - val CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( - recipeId = "1", - note = "2 oz of white sugar", - quantity = 1.0, - unit = null, - food = null, - title = null, - ) - - val CAKE_BREAD_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( - recipeId = "1", - note = "2 oz of white bread", - quantity = 1.0, - unit = null, - food = null, - title = null, - ) - - val FULL_CAKE_INFO_ENTITY = FullRecipeEntity( - recipeEntity = CAKE_RECIPE_ENTITY, - recipeSummaryEntity = CAKE_RECIPE_SUMMARY_ENTITY, - recipeIngredients = listOf( - CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, - CAKE_BREAD_RECIPE_INGREDIENT_ENTITY, - ), - recipeInstructions = listOf( - MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, - BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY, - ), - ) - - private val PORRIDGE_RECIPE_ENTITY_FULL = RecipeEntity( - remoteId = "2", - recipeYield = "3 servings", - disableAmounts = true, - ) - - private val PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( - recipeId = "2", - note = "2 oz of white milk", - quantity = 1.0, - unit = null, - food = null, - title = null, - ) - - private val PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( - recipeId = "2", - note = "2 oz of white sugar", - quantity = 1.0, - unit = null, - food = null, - title = null, - ) - - private val PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( - recipeId = "2", - text = "Mix the ingredients" - ) - - private val PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( - recipeId = "2", - text = "Boil the ingredients" - ) - - val FULL_PORRIDGE_INFO_ENTITY = FullRecipeEntity( - recipeEntity = PORRIDGE_RECIPE_ENTITY_FULL, - recipeSummaryEntity = PORRIDGE_RECIPE_SUMMARY_ENTITY, - recipeIngredients = listOf( - PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY, - PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY, - ), - recipeInstructions = listOf( - PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY, - PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY, - ) - ) - - val SUGAR_ADD_RECIPE_INGREDIENT_INFO = AddRecipeIngredientInfo("2 oz of white sugar") - - val MILK_ADD_RECIPE_INGREDIENT_INFO = AddRecipeIngredientInfo("2 oz of white milk") - - val BOIL_ADD_RECIPE_INSTRUCTION_INFO = AddRecipeInstructionInfo("Boil the ingredients") - - val MIX_ADD_RECIPE_INSTRUCTION_INFO = AddRecipeInstructionInfo("Mix the ingredients") - - val ADD_RECIPE_INFO_SETTINGS = AddRecipeSettingsInfo(disableComments = false, public = true) - - val PORRIDGE_ADD_RECIPE_INFO = AddRecipeInfo( - name = "Porridge", - description = "A tasty porridge", - recipeYield = "3 servings", - recipeIngredient = listOf( - MILK_ADD_RECIPE_INGREDIENT_INFO, - SUGAR_ADD_RECIPE_INGREDIENT_INFO, - ), - recipeInstructions = listOf( - MIX_ADD_RECIPE_INSTRUCTION_INFO, - BOIL_ADD_RECIPE_INSTRUCTION_INFO, - ), - settings = ADD_RECIPE_INFO_SETTINGS, - ) - - val PORRIDGE_RECIPE_DRAFT = AddRecipeDraft( - recipeName = "Porridge", - recipeDescription = "A tasty porridge", - recipeYield = "3 servings", - recipeInstructions = listOf("Mix the ingredients", "Boil the ingredients"), - recipeIngredients = listOf("2 oz of white milk", "2 oz of white sugar"), - isRecipePublic = true, - areCommentsDisabled = false, - ) - - val PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0 = GetRecipeSummaryResponseV0( - remoteId = 2, - name = "Porridge", - slug = "porridge", - description = "A tasty porridge", - dateAdded = LocalDate.parse("2021-11-12"), - dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), - ) - - val PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1 = GetRecipeSummaryResponseV1( - remoteId = "2", - name = "Porridge", - slug = "porridge", - description = "A tasty porridge", - dateAdded = LocalDate.parse("2021-11-12"), - dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), - ) - - val VERSION_RESPONSE_V0 = VersionResponseV0("v0.5.6") - - val VERSION_INFO_V0 = VersionInfo("v0.5.6") - - val VERSION_RESPONSE_V1 = VersionResponseV1("v1.0.0-beta05") - - val VERSION_INFO_V1 = VersionInfo("v1.0.0-beta05") - - val MILK_RECIPE_INGREDIENT_RESPONSE_V0 = GetRecipeIngredientResponseV0("2 oz of white milk") - - val SUGAR_RECIPE_INGREDIENT_RESPONSE_V0 = GetRecipeIngredientResponseV0("2 oz of white sugar") - - val MILK_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1( - note = "2 oz of white milk", - quantity = 1.0, - unit = null, - food = null, - title = null, - ) - - val SUGAR_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1( - note = "2 oz of white sugar", - quantity = 1.0, - unit = null, - food = null, - title = null, - ) - - val MILK_RECIPE_INGREDIENT_INFO = RecipeIngredientInfo( - note = "2 oz of white milk", - quantity = 1.0, - unit = null, - food = null, - title = null, - ) - - val MIX_RECIPE_INSTRUCTION_RESPONSE_V0 = GetRecipeInstructionResponseV0("Mix the ingredients") - - val BOIL_RECIPE_INSTRUCTION_RESPONSE_V0 = GetRecipeInstructionResponseV0("Boil the ingredients") - - val MIX_RECIPE_INSTRUCTION_RESPONSE_V1 = GetRecipeInstructionResponseV1("Mix the ingredients") - - val BOIL_RECIPE_INSTRUCTION_RESPONSE_V1 = GetRecipeInstructionResponseV1("Boil the ingredients") - - val MIX_RECIPE_INSTRUCTION_INFO = RecipeInstructionInfo("Mix the ingredients") - - val PORRIDGE_RECIPE_RESPONSE_V0 = GetRecipeResponseV0( - remoteId = 2, - name = "Porridge", - recipeYield = "3 servings", - recipeIngredients = listOf( - SUGAR_RECIPE_INGREDIENT_RESPONSE_V0, - MILK_RECIPE_INGREDIENT_RESPONSE_V0, - ), - recipeInstructions = listOf( - MIX_RECIPE_INSTRUCTION_RESPONSE_V0, - BOIL_RECIPE_INSTRUCTION_RESPONSE_V0 - ), - ) - - val PORRIDGE_RECIPE_RESPONSE_V1 = GetRecipeResponseV1( - remoteId = "2", - name = "Porridge", - recipeYield = "3 servings", - recipeIngredients = listOf( - SUGAR_RECIPE_INGREDIENT_RESPONSE_V1, - MILK_RECIPE_INGREDIENT_RESPONSE_V1, - ), - recipeInstructions = listOf( - MIX_RECIPE_INSTRUCTION_RESPONSE_V1, - BOIL_RECIPE_INSTRUCTION_RESPONSE_V1 - ), - settings = GetRecipeSettingsResponseV1(disableAmount = true), - ) - - val MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V0 = AddRecipeInstructionV0("Mix the ingredients") - - val BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V0 = AddRecipeInstructionV0("Boil the ingredients") - - val SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V0 = AddRecipeIngredientV0("2 oz of white sugar") - - val MILK_ADD_RECIPE_INGREDIENT_REQUEST_V0 = AddRecipeIngredientV0("2 oz of white milk") - - val ADD_RECIPE_REQUEST_SETTINGS_V0 = AddRecipeSettingsV0(disableComments = false, public = true) - - val PORRIDGE_ADD_RECIPE_REQUEST_V0 = AddRecipeRequestV0( - name = "Porridge", - description = "A tasty porridge", - recipeYield = "3 servings", - recipeInstructions = listOf( - MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V0, - BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V0, - ), - recipeIngredient = listOf( - MILK_ADD_RECIPE_INGREDIENT_REQUEST_V0, - SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V0, - ), - settings = ADD_RECIPE_REQUEST_SETTINGS_V0 - ) - - val MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V1 = AddRecipeInstructionV1( - id = "1", - text = "Mix the ingredients", - ingredientReferences = emptyList() - ) - - val BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V1 = AddRecipeInstructionV1( - id = "2", - text = "Boil the ingredients", - ingredientReferences = emptyList() - ) - - val SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V1 = AddRecipeIngredientV1( - id = "3", - note = "2 oz of white sugar" - ) - - val MILK_ADD_RECIPE_INGREDIENT_REQUEST_V1 = AddRecipeIngredientV1( - id = "4", - note = "2 oz of white milk" - ) - - val ADD_RECIPE_REQUEST_SETTINGS_V1 = AddRecipeSettingsV1(disableComments = false, public = true) - - val PORRIDGE_CREATE_RECIPE_REQUEST_V1 = CreateRecipeRequestV1(name = "Porridge") - - val PORRIDGE_UPDATE_RECIPE_REQUEST_V1 = UpdateRecipeRequestV1( - description = "A tasty porridge", - recipeYield = "3 servings", - recipeInstructions = listOf( - MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V1, - BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V1, - ), - recipeIngredient = listOf( - MILK_ADD_RECIPE_INGREDIENT_REQUEST_V1, - SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V1, - ), - settings = ADD_RECIPE_REQUEST_SETTINGS_V1 - ) -} \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModelTest.kt index e069e6d..a700c7f 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModelTest.kt @@ -6,10 +6,13 @@ import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.BaseUnitTest +import gq.kirmanak.mealient.ui.ActivityUiState +import gq.kirmanak.mealient.ui.ActivityUiStateController import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import org.junit.Before import org.junit.Test @@ -28,6 +31,9 @@ class MainActivityViewModelTest : BaseUnitTest() { @MockK(relaxUnitFun = true) lateinit var recipeRepo: RecipeRepo + @MockK(relaxUnitFun = true) + lateinit var activityUiStateController: ActivityUiStateController + private lateinit var subject: MainActivityViewModel @Before @@ -36,12 +42,17 @@ class MainActivityViewModelTest : BaseUnitTest() { every { authRepo.isAuthorizedFlow } returns emptyFlow() coEvery { disclaimerStorage.isDisclaimerAccepted() } returns true coEvery { serverInfoRepo.getUrl() } returns TEST_BASE_URL + every { activityUiStateController.getUiStateFlow() } returns MutableStateFlow( + ActivityUiState() + ) + coEvery { serverInfoRepo.versionUpdates() } returns emptyFlow() subject = MainActivityViewModel( authRepo = authRepo, logger = logger, disclaimerStorage = disclaimerStorage, serverInfoRepo = serverInfoRepo, recipeRepo = recipeRepo, + activityUiStateController = activityUiStateController, ) } diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt index 5709d10..f869174 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt @@ -2,8 +2,8 @@ package gq.kirmanak.mealient.ui.add import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.add.AddRecipeRepo +import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_INFO import gq.kirmanak.mealient.test.BaseUnitTest -import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_INFO import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt index 59b10ea..9be67d1 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt @@ -5,6 +5,7 @@ import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.datasource.NetworkError +import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.ui.OperationUiState @@ -31,6 +32,9 @@ class BaseURLViewModelTest : BaseUnitTest() { @MockK(relaxUnitFun = true) lateinit var recipeRepo: RecipeRepo + @MockK(relaxUnitFun = true) + lateinit var shoppingListsRepo: ShoppingListsRepo + lateinit var subject: BaseURLViewModel @Before @@ -41,6 +45,7 @@ class BaseURLViewModelTest : BaseUnitTest() { authRepo = authRepo, recipeRepo = recipeRepo, logger = logger, + shoppingListsRepo = shoppingListsRepo, ) } diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModelTest.kt index cb071cd..4b9dcea 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModelTest.kt @@ -4,8 +4,8 @@ import androidx.lifecycle.asFlow import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo +import gq.kirmanak.mealient.database.CAKE_RECIPE_SUMMARY_ENTITY import gq.kirmanak.mealient.test.BaseUnitTest -import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModelTest.kt index 82f977d..0914de4 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModelTest.kt @@ -3,8 +3,8 @@ package gq.kirmanak.mealient.ui.recipes.info import androidx.lifecycle.asFlow import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.recipes.RecipeRepo +import gq.kirmanak.mealient.database.FULL_CAKE_INFO_ENTITY import gq.kirmanak.mealient.test.BaseUnitTest -import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY import io.mockk.coEvery import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/architecture/build.gradle.kts b/architecture/build.gradle.kts index 011e9ed..ea05266 100644 --- a/architecture/build.gradle.kts +++ b/architecture/build.gradle.kts @@ -11,4 +11,10 @@ android { dependencies { implementation(libs.google.dagger.hiltAndroid) kapt(libs.google.dagger.hiltCompiler) + + testImplementation(libs.jetbrains.kotlinx.coroutinesTest) + testImplementation(libs.androidx.test.junit) + testImplementation(libs.androidx.coreTesting) + testImplementation(libs.google.truth) + testImplementation(project(":testing")) } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/FlowExtensions.kt b/architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/FlowExtensions.kt similarity index 93% rename from app/src/main/java/gq/kirmanak/mealient/extensions/FlowExtensions.kt rename to architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/FlowExtensions.kt index eedafc0..497b717 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/FlowExtensions.kt +++ b/architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/FlowExtensions.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.extensions +package gq.kirmanak.mealient.architecture import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector diff --git a/app/src/test/java/gq/kirmanak/mealient/extensions/FlowExtensionsKtTest.kt b/architecture/src/test/kotlin/gq/kirmanak/mealient/architecture/FlowExtensionsKtTest.kt similarity index 97% rename from app/src/test/java/gq/kirmanak/mealient/extensions/FlowExtensionsKtTest.kt rename to architecture/src/test/kotlin/gq/kirmanak/mealient/architecture/FlowExtensionsKtTest.kt index 2fa9781..d61815a 100644 --- a/app/src/test/java/gq/kirmanak/mealient/extensions/FlowExtensionsKtTest.kt +++ b/architecture/src/test/kotlin/gq/kirmanak/mealient/architecture/FlowExtensionsKtTest.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.extensions +package gq.kirmanak.mealient.architecture import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.test.BaseUnitTest diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 96d504c..ac58325 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -19,5 +19,9 @@ gradlePlugin { id = "gq.kirmanak.mealient.library" implementationClass = "AndroidLibraryConventionPlugin" } + register("compose") { + id = "gq.kirmanak.mealient.compose" + implementationClass = "AndroidLibraryComposeConventionPlugin" + } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt new file mode 100644 index 0000000..c1b3715 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -0,0 +1,16 @@ +import com.android.build.gradle.LibraryExtension +import gq.kirmanak.mealient.configureAndroidCompose +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +class AndroidLibraryComposeConventionPlugin : Plugin { + + override fun apply(target: Project) { + with(target) { + extensions.configure { + configureAndroidCompose(this) + } + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/AndroidCompose.kt new file mode 100644 index 0000000..538a8a3 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/AndroidCompose.kt @@ -0,0 +1,74 @@ +package gq.kirmanak.mealient + +import com.android.build.api.dsl.CommonExtension +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.internal.dsl.BaseAppModuleExtension +import org.gradle.api.Action +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.Project +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.dependencies +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +internal fun Project.configureAndroidCompose( + commonExtension: CommonExtension<*, *, *, *>, +) { + val variants = when (commonExtension) { + is BaseAppModuleExtension -> commonExtension.applicationVariants + is LibraryExtension -> commonExtension.libraryVariants + else -> error("Unsupported extension type") + } + + commonExtension.apply { + buildFeatures { + compose = true + } + + composeOptions { + val version = libs.findVersion("composeKotlinCompilerExtension") + kotlinCompilerExtensionVersion = version.get().toString() + } + + // Add compose-destinations generated code to Gradle source sets + variants.all { + kotlin.sourceSets { + getByName(name) { + kotlin.srcDir("build/generated/ksp/$name/kotlin") + } + } + } + + dependencies { + val bom = library("androidx-compose-bom") + add("implementation", platform(bom)) + add("androidTestImplementation", platform(bom)) + + add("implementation", library("androidx-compose-material3")) + add("implementation", library("androidx-compose-ui-toolingPreview")) + add("implementation", library("androidx-compose-runtime-livedata")) + add("implementation", library("androidx-lifecycle-viewmodelCompose")) + add("implementation", library("google-accompanist-themeadapter-material3")) + add("debugImplementation", library("androidx-compose-ui-tooling")) + add("debugImplementation", library("androidx-compose-ui-testManifest")) + add("androidTestImplementation", library("androidx-compose-ui-testJunit")) + add("implementation", library("composeDestinations-core")) + add("ksp", library("composeDestinations-ksp")) + } + } +} + +private fun Project.library(name: String): Provider { + return libs.findLibrary(name).get() +} + +private val Project.kotlin: KotlinAndroidProjectExtension + get() = (this as ExtensionAware).extensions.getByName("kotlin") as KotlinAndroidProjectExtension + +private fun KotlinAndroidProjectExtension.sourceSets(configure: Action>): Unit = + (this as ExtensionAware).extensions.configure("sourceSets", configure) + + + diff --git a/build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/KotlinAndroid.kt index 9eb9290..e8549cf 100644 --- a/build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/gq/kirmanak/mealient/KotlinAndroid.kt @@ -27,7 +27,11 @@ internal fun Project.configureKotlinAndroid( } lint { - disable += listOf("ObsoleteLintCustomCheck", "IconMissingDensityFolder") + disable += listOf( + "ObsoleteLintCustomCheck", + "IconMissingDensityFolder", + "MissingTranslation" + ) enable += listOf( "ConvertToWebp", "DuplicateStrings", diff --git a/database/build.gradle.kts b/database/build.gradle.kts index 7f5ab6f..16cc132 100644 --- a/database/build.gradle.kts +++ b/database/build.gradle.kts @@ -6,30 +6,27 @@ plugins { } android { - defaultConfig { - ksp { - arg("room.schemaLocation", "$projectDir/schemas") - } - } - namespace = "gq.kirmanak.mealient.database" } dependencies { + implementation(project(":logging")) + testImplementation(project(":testing")) + testImplementation(project(":database_test")) + implementation(libs.google.dagger.hiltAndroid) kapt(libs.google.dagger.hiltCompiler) kaptTest(libs.google.dagger.hiltAndroidCompiler) testImplementation(libs.google.dagger.hiltAndroidTesting) - // withTransaction is used in the app module - api(libs.androidx.room.ktx) + implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.paging) ksp(libs.androidx.room.compiler) testImplementation(libs.androidx.room.testing) - implementation(libs.jetbrains.kotlinx.datetime) + api(libs.jetbrains.kotlinx.datetime) implementation(libs.jetbrains.kotlinx.coroutinesAndroid) testImplementation(libs.jetbrains.kotlinx.coroutinesTest) diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/1.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/1.json deleted file mode 100644 index 29408b7..0000000 --- a/database/schemas/gq.kirmanak.mealient.database.AppDb/1.json +++ /dev/null @@ -1,404 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "cac9e9a2f4082b071336eff342e0c01f", - "entities": [ - { - "tableName": "categories", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_categories_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "category_recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "categoryId", - "columnName": "category_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "category_id", - "recipe_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_category_recipe_category_id_recipe_id", - "unique": true, - "columnNames": [ - "category_id", - "recipe_id" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)" - }, - { - "name": "index_category_recipe_recipe_id", - "unique": false, - "columnNames": [ - "recipe_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" - } - ], - "foreignKeys": [ - { - "table": "categories", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "category_id" - ], - "referencedColumns": [ - "local_id" - ] - }, - { - "table": "recipe_summaries", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "recipe_id" - ], - "referencedColumns": [ - "remote_id" - ] - } - ] - }, - { - "tableName": "tags", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_tags_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tag_recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` INTEGER NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tagId", - "columnName": "tag_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "tag_id", - "recipe_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_tag_recipe_recipe_id", - "unique": false, - "columnNames": [ - "recipe_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" - } - ], - "foreignKeys": [ - { - "table": "tags", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "tag_id" - ], - "referencedColumns": [ - "local_id" - ] - }, - { - "table": "recipe_summaries", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "recipe_id" - ], - "referencedColumns": [ - "remote_id" - ] - } - ] - }, - { - "tableName": "recipe_summaries", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "slug", - "columnName": "slug", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "image", - "columnName": "image", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rating", - "columnName": "rating", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "dateAdded", - "columnName": "date_added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dateUpdated", - "columnName": "date_updated", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` INTEGER NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeYield", - "columnName": "recipe_yield", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_ingredient", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `unit` TEXT NOT NULL, `food` TEXT NOT NULL, `disable_amount` INTEGER NOT NULL, `quantity` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "unit", - "columnName": "unit", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "food", - "columnName": "food", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "disableAmount", - "columnName": "disable_amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "quantity", - "columnName": "quantity", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_instruction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "text", - "columnName": "text", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cac9e9a2f4082b071336eff342e0c01f')" - ] - } -} \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/2.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/2.json deleted file mode 100644 index 1bf511b..0000000 --- a/database/schemas/gq.kirmanak.mealient.database.AppDb/2.json +++ /dev/null @@ -1,404 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 2, - "identityHash": "cac9e9a2f4082b071336eff342e0c01f", - "entities": [ - { - "tableName": "categories", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_categories_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "category_recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "categoryId", - "columnName": "category_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "category_id", - "recipe_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_category_recipe_category_id_recipe_id", - "unique": true, - "columnNames": [ - "category_id", - "recipe_id" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)" - }, - { - "name": "index_category_recipe_recipe_id", - "unique": false, - "columnNames": [ - "recipe_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" - } - ], - "foreignKeys": [ - { - "table": "categories", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "category_id" - ], - "referencedColumns": [ - "local_id" - ] - }, - { - "table": "recipe_summaries", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "recipe_id" - ], - "referencedColumns": [ - "remote_id" - ] - } - ] - }, - { - "tableName": "tags", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_tags_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tag_recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` INTEGER NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tagId", - "columnName": "tag_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "tag_id", - "recipe_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_tag_recipe_recipe_id", - "unique": false, - "columnNames": [ - "recipe_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" - } - ], - "foreignKeys": [ - { - "table": "tags", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "tag_id" - ], - "referencedColumns": [ - "local_id" - ] - }, - { - "table": "recipe_summaries", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "recipe_id" - ], - "referencedColumns": [ - "remote_id" - ] - } - ] - }, - { - "tableName": "recipe_summaries", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "slug", - "columnName": "slug", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "image", - "columnName": "image", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rating", - "columnName": "rating", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "dateAdded", - "columnName": "date_added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dateUpdated", - "columnName": "date_updated", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` INTEGER NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeYield", - "columnName": "recipe_yield", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_ingredient", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `unit` TEXT NOT NULL, `food` TEXT NOT NULL, `disable_amount` INTEGER NOT NULL, `quantity` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "unit", - "columnName": "unit", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "food", - "columnName": "food", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "disableAmount", - "columnName": "disable_amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "quantity", - "columnName": "quantity", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_instruction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "text", - "columnName": "text", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cac9e9a2f4082b071336eff342e0c01f')" - ] - } -} \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/3.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/3.json deleted file mode 100644 index 9698138..0000000 --- a/database/schemas/gq.kirmanak.mealient.database.AppDb/3.json +++ /dev/null @@ -1,404 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 3, - "identityHash": "28c896eb34e95c0cff33148178252f72", - "entities": [ - { - "tableName": "categories", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_categories_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "category_recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "categoryId", - "columnName": "category_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "category_id", - "recipe_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_category_recipe_category_id_recipe_id", - "unique": true, - "columnNames": [ - "category_id", - "recipe_id" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)" - }, - { - "name": "index_category_recipe_recipe_id", - "unique": false, - "columnNames": [ - "recipe_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" - } - ], - "foreignKeys": [ - { - "table": "categories", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "category_id" - ], - "referencedColumns": [ - "local_id" - ] - }, - { - "table": "recipe_summaries", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "recipe_id" - ], - "referencedColumns": [ - "remote_id" - ] - } - ] - }, - { - "tableName": "tags", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_tags_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tag_recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tagId", - "columnName": "tag_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "tag_id", - "recipe_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_tag_recipe_recipe_id", - "unique": false, - "columnNames": [ - "recipe_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" - } - ], - "foreignKeys": [ - { - "table": "tags", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "tag_id" - ], - "referencedColumns": [ - "local_id" - ] - }, - { - "table": "recipe_summaries", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "recipe_id" - ], - "referencedColumns": [ - "remote_id" - ] - } - ] - }, - { - "tableName": "recipe_summaries", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "slug", - "columnName": "slug", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "image", - "columnName": "image", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rating", - "columnName": "rating", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "dateAdded", - "columnName": "date_added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dateUpdated", - "columnName": "date_updated", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "recipeYield", - "columnName": "recipe_yield", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_ingredient", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `unit` TEXT NOT NULL, `food` TEXT NOT NULL, `disable_amount` INTEGER NOT NULL, `quantity` REAL NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "unit", - "columnName": "unit", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "food", - "columnName": "food", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "disableAmount", - "columnName": "disable_amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "quantity", - "columnName": "quantity", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_instruction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "text", - "columnName": "text", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '28c896eb34e95c0cff33148178252f72')" - ] - } -} \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/4.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/4.json deleted file mode 100644 index 6b273aa..0000000 --- a/database/schemas/gq.kirmanak.mealient.database.AppDb/4.json +++ /dev/null @@ -1,410 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 4, - "identityHash": "13be83018f147e1f6e864790656da4a7", - "entities": [ - { - "tableName": "categories", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_categories_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "category_recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "categoryId", - "columnName": "category_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "category_id", - "recipe_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_category_recipe_category_id_recipe_id", - "unique": true, - "columnNames": [ - "category_id", - "recipe_id" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)" - }, - { - "name": "index_category_recipe_recipe_id", - "unique": false, - "columnNames": [ - "recipe_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" - } - ], - "foreignKeys": [ - { - "table": "categories", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "category_id" - ], - "referencedColumns": [ - "local_id" - ] - }, - { - "table": "recipe_summaries", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "recipe_id" - ], - "referencedColumns": [ - "remote_id" - ] - } - ] - }, - { - "tableName": "tags", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_tags_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tag_recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tagId", - "columnName": "tag_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "tag_id", - "recipe_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_tag_recipe_recipe_id", - "unique": false, - "columnNames": [ - "recipe_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" - } - ], - "foreignKeys": [ - { - "table": "tags", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "tag_id" - ], - "referencedColumns": [ - "local_id" - ] - }, - { - "table": "recipe_summaries", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "recipe_id" - ], - "referencedColumns": [ - "remote_id" - ] - } - ] - }, - { - "tableName": "recipe_summaries", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "slug", - "columnName": "slug", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "image", - "columnName": "image", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rating", - "columnName": "rating", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "dateAdded", - "columnName": "date_added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dateUpdated", - "columnName": "date_updated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "imageId", - "columnName": "image_id", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "recipeYield", - "columnName": "recipe_yield", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_ingredient", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `unit` TEXT NOT NULL, `food` TEXT NOT NULL, `disable_amount` INTEGER NOT NULL, `quantity` REAL NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "unit", - "columnName": "unit", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "food", - "columnName": "food", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "disableAmount", - "columnName": "disable_amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "quantity", - "columnName": "quantity", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_instruction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "text", - "columnName": "text", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '13be83018f147e1f6e864790656da4a7')" - ] - } -} \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/5.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/5.json deleted file mode 100644 index 00986f8..0000000 --- a/database/schemas/gq.kirmanak.mealient.database.AppDb/5.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 5, - "identityHash": "e75a1e16503fdf60c62b7f9d17ec0bc6", - "entities": [ - { - "tableName": "categories", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_categories_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "category_recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "categoryId", - "columnName": "category_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "category_id", - "recipe_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_category_recipe_category_id_recipe_id", - "unique": true, - "columnNames": [ - "category_id", - "recipe_id" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)" - }, - { - "name": "index_category_recipe_recipe_id", - "unique": false, - "columnNames": [ - "recipe_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" - } - ], - "foreignKeys": [ - { - "table": "categories", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "category_id" - ], - "referencedColumns": [ - "local_id" - ] - }, - { - "table": "recipe_summaries", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "recipe_id" - ], - "referencedColumns": [ - "remote_id" - ] - } - ] - }, - { - "tableName": "tags", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_tags_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tag_recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tagId", - "columnName": "tag_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "tag_id", - "recipe_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_tag_recipe_recipe_id", - "unique": false, - "columnNames": [ - "recipe_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" - } - ], - "foreignKeys": [ - { - "table": "tags", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "tag_id" - ], - "referencedColumns": [ - "local_id" - ] - }, - { - "table": "recipe_summaries", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "recipe_id" - ], - "referencedColumns": [ - "remote_id" - ] - } - ] - }, - { - "tableName": "recipe_summaries", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "slug", - "columnName": "slug", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "image", - "columnName": "image", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rating", - "columnName": "rating", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "dateAdded", - "columnName": "date_added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dateUpdated", - "columnName": "date_updated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "imageId", - "columnName": "image_id", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "recipeYield", - "columnName": "recipe_yield", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_ingredient", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_instruction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "text", - "columnName": "text", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e75a1e16503fdf60c62b7f9d17ec0bc6')" - ] - } -} \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/6.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/6.json deleted file mode 100644 index 81e2dc6..0000000 --- a/database/schemas/gq.kirmanak.mealient.database.AppDb/6.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 6, - "identityHash": "f6e28dd617e4d4a6843a7865c9da736d", - "entities": [ - { - "tableName": "recipe_summaries", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "slug", - "columnName": "slug", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateAdded", - "columnName": "date_added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dateUpdated", - "columnName": "date_updated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "imageId", - "columnName": "image_id", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "recipeYield", - "columnName": "recipe_yield", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_ingredient", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_instruction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "text", - "columnName": "text", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f6e28dd617e4d4a6843a7865c9da736d')" - ] - } -} \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json deleted file mode 100644 index ca613b7..0000000 --- a/database/schemas/gq.kirmanak.mealient.database.AppDb/7.json +++ /dev/null @@ -1,191 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 7, - "identityHash": "d2679aea13d3c18e58c537164f70e249", - "entities": [ - { - "tableName": "recipe_summaries", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "slug", - "columnName": "slug", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateAdded", - "columnName": "date_added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dateUpdated", - "columnName": "date_updated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "imageId", - "columnName": "image_id", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, `disable_amounts` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "recipeYield", - "columnName": "recipe_yield", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "disableAmounts", - "columnName": "disable_amounts", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_ingredient", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL, `food` TEXT, `unit` TEXT, `quantity` REAL, `title` TEXT)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "food", - "columnName": "food", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "unit", - "columnName": "unit", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "quantity", - "columnName": "quantity", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_instruction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "text", - "columnName": "text", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd2679aea13d3c18e58c537164f70e249')" - ] - } -} \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/8.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/8.json deleted file mode 100644 index 735d017..0000000 --- a/database/schemas/gq.kirmanak.mealient.database.AppDb/8.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 8, - "identityHash": "793673e401425db36544918dae6bf4c1", - "entities": [ - { - "tableName": "recipe_summaries", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, `is_favorite` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "slug", - "columnName": "slug", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateAdded", - "columnName": "date_added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "dateUpdated", - "columnName": "date_updated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "imageId", - "columnName": "image_id", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isFavorite", - "columnName": "is_favorite", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, `disable_amounts` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`remote_id`))", - "fields": [ - { - "fieldPath": "remoteId", - "columnName": "remote_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "recipeYield", - "columnName": "recipe_yield", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "disableAmounts", - "columnName": "disable_amounts", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - } - ], - "primaryKey": { - "columnNames": [ - "remote_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_ingredient", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL, `food` TEXT, `unit` TEXT, `quantity` REAL, `title` TEXT)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "note", - "columnName": "note", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "food", - "columnName": "food", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "unit", - "columnName": "unit", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "quantity", - "columnName": "quantity", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "recipe_instruction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "localId", - "columnName": "local_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "recipeId", - "columnName": "recipe_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "text", - "columnName": "text", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "local_id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '793673e401425db36544918dae6bf4c1')" - ] - } -} \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt index 635e776..6f32655 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt @@ -1,45 +1,20 @@ package gq.kirmanak.mealient.database import androidx.room.* -import androidx.room.migration.AutoMigrationSpec import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.entity.* @Database( - version = 8, + version = 10, entities = [ RecipeSummaryEntity::class, RecipeEntity::class, RecipeIngredientEntity::class, RecipeInstructionEntity::class, - ], - exportSchema = true, - autoMigrations = [ - AutoMigration(from = 1, to = 2), - AutoMigration(from = 3, to = 4), - AutoMigration(from = 4, to = 5, spec = AppDb.From4To5Migration::class), - AutoMigration(from = 5, to = 6, spec = AppDb.From5To6Migration::class), - AutoMigration(from = 6, to = 7), - AutoMigration(from = 7, to = 8), ] ) @TypeConverters(RoomTypeConverters::class) -abstract class AppDb : RoomDatabase() { +internal abstract class AppDb : RoomDatabase() { + abstract fun recipeDao(): RecipeDao - - @DeleteColumn(tableName = "recipe_instruction", columnName = "title") - @DeleteColumn(tableName = "recipe_ingredient", columnName = "title") - @DeleteColumn(tableName = "recipe_ingredient", columnName = "unit") - @DeleteColumn(tableName = "recipe_ingredient", columnName = "food") - @DeleteColumn(tableName = "recipe_ingredient", columnName = "disable_amount") - @DeleteColumn(tableName = "recipe_ingredient", columnName = "quantity") - class From4To5Migration : AutoMigrationSpec - - @DeleteColumn(tableName = "recipe_summaries", columnName = "image") - @DeleteColumn(tableName = "recipe_summaries", columnName = "rating") - @DeleteTable(tableName = "tag_recipe") - @DeleteTable(tableName = "tags") - @DeleteTable(tableName = "categories") - @DeleteTable(tableName = "category_recipe") - class From5To6Migration : AutoMigrationSpec } \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/DatabaseModule.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/DatabaseModule.kt index c9e96d2..50ebcf7 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/DatabaseModule.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/DatabaseModule.kt @@ -2,23 +2,35 @@ package gq.kirmanak.mealient.database import android.content.Context import androidx.room.Room +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import gq.kirmanak.mealient.database.recipe.RecipeDao +import gq.kirmanak.mealient.database.recipe.RecipeStorage +import gq.kirmanak.mealient.database.recipe.RecipeStorageImpl import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -interface DatabaseModule { +internal interface DatabaseModule { companion object { @Provides @Singleton fun createDb(@ApplicationContext context: Context): AppDb = Room.databaseBuilder(context, AppDb::class.java, "app.db") - .fallbackToDestructiveMigrationFrom(2) + .fallbackToDestructiveMigration() .build() + + @Provides + @Singleton + fun provideRecipeDao(db: AppDb): RecipeDao = db.recipeDao() } + + @Binds + @Singleton + fun provideRecipeStorage(recipeStorageImpl: RecipeStorageImpl): RecipeStorage } \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt index e7ef953..4f758c7 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt @@ -5,25 +5,30 @@ import androidx.room.* import gq.kirmanak.mealient.database.recipe.entity.* @Dao -interface RecipeDao { - @Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC") +internal interface RecipeDao { + @Query("SELECT * FROM recipe_summaries ORDER BY recipe_summaries_date_added DESC") fun queryRecipesByPages(): PagingSource - @Query("SELECT * FROM recipe_summaries WHERE recipe_summaries.name LIKE '%' || :query || '%' ORDER BY date_added DESC") + @Query("SELECT * FROM recipe_summaries WHERE recipe_summaries_name LIKE '%' || :query || '%' ORDER BY recipe_summaries_date_added DESC") fun queryRecipesByPages(query: String): PagingSource + @Transaction @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertRecipes(recipeSummaryEntity: Iterable) + suspend fun insertRecipeSummaries(recipeSummaryEntity: Iterable) + @Transaction @Query("DELETE FROM recipe_summaries") suspend fun removeAllRecipes() - @Query("SELECT * FROM recipe_summaries ORDER BY date_updated DESC") + @Query("SELECT * FROM recipe_summaries ORDER BY recipe_summaries_date_added DESC") suspend fun queryAllRecipes(): List @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertRecipe(recipe: RecipeEntity) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertRecipes(recipe: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertRecipeInstructions(instructions: List) @@ -32,19 +37,25 @@ interface RecipeDao { @Transaction @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) // The lint is wrong, the columns are actually used - @Query("SELECT * FROM recipe JOIN recipe_summaries ON recipe.remote_id = recipe_summaries.remote_id JOIN recipe_ingredient ON recipe_ingredient.recipe_id = recipe.remote_id JOIN recipe_instruction ON recipe_instruction.recipe_id = recipe.remote_id WHERE recipe.remote_id = :recipeId") - suspend fun queryFullRecipeInfo(recipeId: String): FullRecipeEntity? + @Query( + "SELECT * FROM recipe " + + "JOIN recipe_summaries USING(recipe_id) " + + "JOIN recipe_ingredient USING(recipe_id) " + + "JOIN recipe_instruction USING(recipe_id) " + + "WHERE recipe.recipe_id = :recipeId" + ) + suspend fun queryFullRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions? - @Query("DELETE FROM recipe_ingredient WHERE recipe_id = :recipeId") - suspend fun deleteRecipeIngredients(recipeId: String) + @Query("DELETE FROM recipe_ingredient WHERE recipe_id IN (:recipeIds)") + suspend fun deleteRecipeIngredients(vararg recipeIds: String) - @Query("DELETE FROM recipe_instruction WHERE recipe_id = :recipeId") - suspend fun deleteRecipeInstructions(recipeId: String) + @Query("DELETE FROM recipe_instruction WHERE recipe_id IN (:recipeIds)") + suspend fun deleteRecipeInstructions(vararg recipeIds: String) - @Query("UPDATE recipe_summaries SET is_favorite = 1 WHERE slug IN (:favorites)") + @Query("UPDATE recipe_summaries SET recipe_summaries_is_favorite = 1 WHERE recipe_summaries_slug IN (:favorites)") suspend fun setFavorite(favorites: List) - @Query("UPDATE recipe_summaries SET is_favorite = 0 WHERE slug NOT IN (:favorites)") + @Query("UPDATE recipe_summaries SET recipe_summaries_is_favorite = 0 WHERE recipe_summaries_slug NOT IN (:favorites)") suspend fun setNonFavorite(favorites: List) @Delete diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeStorage.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeStorage.kt new file mode 100644 index 0000000..40a7225 --- /dev/null +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeStorage.kt @@ -0,0 +1,30 @@ +package gq.kirmanak.mealient.database.recipe + +import androidx.paging.PagingSource +import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions + +interface RecipeStorage { + suspend fun saveRecipes(recipes: List) + + fun queryRecipes(query: String?): PagingSource + + suspend fun refreshAll(recipes: List) + + suspend fun clearAllLocalData() + + suspend fun saveRecipeInfo( + recipe: RecipeEntity, + ingredients: List, + instructions: List + ) + + suspend fun queryRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions? + + suspend fun updateFavoriteRecipes(favorites: List) + + suspend fun deleteRecipe(entity: RecipeSummaryEntity) +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeStorageImpl.kt similarity index 66% rename from app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt rename to database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeStorageImpl.kt index b59f234..9d37256 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeStorageImpl.kt @@ -1,29 +1,27 @@ -package gq.kirmanak.mealient.data.recipes.db +package gq.kirmanak.mealient.database.recipe import androidx.paging.PagingSource import androidx.room.withTransaction -import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo import gq.kirmanak.mealient.database.AppDb -import gq.kirmanak.mealient.database.recipe.RecipeDao -import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity -import gq.kirmanak.mealient.extensions.toRecipeEntity -import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity -import gq.kirmanak.mealient.extensions.toRecipeInstructionEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject import javax.inject.Singleton @Singleton -class RecipeStorageImpl @Inject constructor( +internal class RecipeStorageImpl @Inject constructor( private val db: AppDb, private val logger: Logger, + private val recipeDao: RecipeDao, ) : RecipeStorage { - private val recipeDao: RecipeDao by lazy { db.recipeDao() } override suspend fun saveRecipes(recipes: List) { logger.v { "saveRecipes() called with $recipes" } - db.withTransaction { recipeDao.insertRecipes(recipes) } + recipeDao.insertRecipeSummaries(recipes) } override fun queryRecipes(query: String?): PagingSource { @@ -42,31 +40,27 @@ class RecipeStorageImpl @Inject constructor( override suspend fun clearAllLocalData() { logger.v { "clearAllLocalData() called" } - db.withTransaction { - recipeDao.removeAllRecipes() - } + recipeDao.removeAllRecipes() } - override suspend fun saveRecipeInfo(recipe: FullRecipeInfo) { + override suspend fun saveRecipeInfo( + recipe: RecipeEntity, + ingredients: List, + instructions: List + ) { logger.v { "saveRecipeInfo() called with: recipe = $recipe" } db.withTransaction { - recipeDao.insertRecipe(recipe.toRecipeEntity()) + recipeDao.insertRecipe(recipe) recipeDao.deleteRecipeIngredients(recipe.remoteId) - val ingredients = recipe.recipeIngredients.map { - it.toRecipeIngredientEntity(recipe.remoteId) - } recipeDao.insertRecipeIngredients(ingredients) recipeDao.deleteRecipeInstructions(recipe.remoteId) - val instructions = recipe.recipeInstructions.map { - it.toRecipeInstructionEntity(recipe.remoteId) - } recipeDao.insertRecipeInstructions(instructions) } } - override suspend fun queryRecipeInfo(recipeId: String): FullRecipeEntity? { + override suspend fun queryRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions? { logger.v { "queryRecipeInfo() called with: recipeId = $recipeId" } val fullRecipeInfo = recipeDao.queryFullRecipeInfo(recipeId) logger.v { "queryRecipeInfo() returned: $fullRecipeInfo" } diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt index 9d0c60d..1b93e20 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt @@ -6,7 +6,7 @@ import androidx.room.PrimaryKey @Entity(tableName = "recipe") data class RecipeEntity( - @PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: String, + @PrimaryKey @ColumnInfo(name = "recipe_id") val remoteId: String, @ColumnInfo(name = "recipe_yield") val recipeYield: String, - @ColumnInfo(name = "disable_amounts", defaultValue = "true") val disableAmounts: Boolean, + @ColumnInfo(name = "recipe_disable_amounts", defaultValue = "true") val disableAmounts: Boolean, ) diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt index 0e12f1f..10d9416 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt @@ -2,17 +2,28 @@ package gq.kirmanak.mealient.database.recipe.entity import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.PrimaryKey -@Entity(tableName = "recipe_ingredient") +@Entity( + tableName = "recipe_ingredient", + foreignKeys = [ + ForeignKey( + entity = RecipeEntity::class, + parentColumns = ["recipe_id"], + childColumns = ["recipe_id"], + onDelete = ForeignKey.CASCADE + ) + ] +) data class RecipeIngredientEntity( - @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, - @ColumnInfo(name = "recipe_id") val recipeId: String, - @ColumnInfo(name = "note") val note: String, - @ColumnInfo(name = "food") val food: String?, - @ColumnInfo(name = "unit") val unit: String?, - @ColumnInfo(name = "quantity") val quantity: Double?, - @ColumnInfo(name = "title") val title: String?, + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "recipe_ingredient_local_id") val localId: Long = 0, + @ColumnInfo(name = "recipe_id", index = true) val recipeId: String, + @ColumnInfo(name = "recipe_ingredient_note") val note: String, + @ColumnInfo(name = "recipe_ingredient_food") val food: String?, + @ColumnInfo(name = "recipe_ingredient_unit") val unit: String?, + @ColumnInfo(name = "recipe_ingredient_quantity") val quantity: Double?, + @ColumnInfo(name = "recipe_ingredient_title") val title: String?, ) { override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeInstructionEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeInstructionEntity.kt index eb4ab61..2e37976 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeInstructionEntity.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeInstructionEntity.kt @@ -2,13 +2,24 @@ package gq.kirmanak.mealient.database.recipe.entity import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.PrimaryKey -@Entity(tableName = "recipe_instruction") +@Entity( + tableName = "recipe_instruction", + foreignKeys = [ + ForeignKey( + entity = RecipeEntity::class, + parentColumns = ["recipe_id"], + childColumns = ["recipe_id"], + onDelete = ForeignKey.CASCADE + ) + ] +) data class RecipeInstructionEntity( - @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, - @ColumnInfo(name = "recipe_id") val recipeId: String, - @ColumnInfo(name = "text") val text: String, + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "recipe_instruction_local_id") val localId: Long = 0, + @ColumnInfo(name = "recipe_id", index = true) val recipeId: String, + @ColumnInfo(name = "recipe_instruction_text") val text: String, ) { override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryEntity.kt index cf54bfa..65ec822 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryEntity.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryEntity.kt @@ -8,12 +8,15 @@ import kotlinx.datetime.LocalDateTime @Entity(tableName = "recipe_summaries") data class RecipeSummaryEntity( - @PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: String, - @ColumnInfo(name = "name") val name: String, - @ColumnInfo(name = "slug") val slug: String, - @ColumnInfo(name = "description") val description: String, - @ColumnInfo(name = "date_added") val dateAdded: LocalDate, - @ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime, - @ColumnInfo(name = "image_id") val imageId: String?, - @ColumnInfo(name = "is_favorite", defaultValue = "false") val isFavorite: Boolean, + @PrimaryKey @ColumnInfo(name = "recipe_id") val remoteId: String, + @ColumnInfo(name = "recipe_summaries_name") val name: String, + @ColumnInfo(name = "recipe_summaries_slug") val slug: String, + @ColumnInfo(name = "recipe_summaries_description") val description: String, + @ColumnInfo(name = "recipe_summaries_date_added") val dateAdded: LocalDate, + @ColumnInfo(name = "recipe_summaries_date_updated") val dateUpdated: LocalDateTime, + @ColumnInfo(name = "recipe_summaries_image_id") val imageId: String?, + @ColumnInfo( + name = "recipe_summaries_is_favorite", + defaultValue = "false" + ) val isFavorite: Boolean, ) \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/FullRecipeEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeWithSummaryAndIngredientsAndInstructions.kt similarity index 69% rename from database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/FullRecipeEntity.kt rename to database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeWithSummaryAndIngredientsAndInstructions.kt index 2c0cab0..51746b0 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/FullRecipeEntity.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeWithSummaryAndIngredientsAndInstructions.kt @@ -3,20 +3,20 @@ package gq.kirmanak.mealient.database.recipe.entity import androidx.room.Embedded import androidx.room.Relation -data class FullRecipeEntity( +data class RecipeWithSummaryAndIngredientsAndInstructions( @Embedded val recipeEntity: RecipeEntity, @Relation( - parentColumn = "remote_id", - entityColumn = "remote_id" + parentColumn = "recipe_id", + entityColumn = "recipe_id" ) val recipeSummaryEntity: RecipeSummaryEntity, @Relation( - parentColumn = "remote_id", + parentColumn = "recipe_id", entityColumn = "recipe_id" ) val recipeIngredients: List, @Relation( - parentColumn = "remote_id", + parentColumn = "recipe_id", entityColumn = "recipe_id" ) val recipeInstructions: List, diff --git a/database/src/test/kotlin/gq/kirmanak/mealient/database/RecipeStorageImplTest.kt b/database/src/test/kotlin/gq/kirmanak/mealient/database/RecipeStorageImplTest.kt new file mode 100644 index 0000000..e60e3af --- /dev/null +++ b/database/src/test/kotlin/gq/kirmanak/mealient/database/RecipeStorageImplTest.kt @@ -0,0 +1,113 @@ +package gq.kirmanak.mealient.database + +import com.google.common.truth.Truth.assertThat +import dagger.hilt.android.testing.HiltAndroidTest +import gq.kirmanak.mealient.database.recipe.RecipeDao +import gq.kirmanak.mealient.database.recipe.RecipeStorageImpl +import gq.kirmanak.mealient.test.HiltRobolectricTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +@OptIn(ExperimentalCoroutinesApi::class) +internal class RecipeStorageImplTest : HiltRobolectricTest() { + + @Inject + lateinit var subject: RecipeStorageImpl + + @Inject + lateinit var recipeDao: RecipeDao + + @Test + fun `when saveRecipes then saves recipes`() = runTest { + subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES) + val actualTags = recipeDao.queryAllRecipes() + assertThat(actualTags).containsExactly( + CAKE_RECIPE_SUMMARY_ENTITY, + PORRIDGE_RECIPE_SUMMARY_ENTITY + ) + } + + @Test + fun `when refreshAll then old recipes aren't preserved`() = runTest { + subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES) + subject.refreshAll(listOf(CAKE_RECIPE_SUMMARY_ENTITY)) + val actual = recipeDao.queryAllRecipes() + assertThat(actual).containsExactly(CAKE_RECIPE_SUMMARY_ENTITY) + } + + @Test + fun `when clearAllLocalData then recipes aren't preserved`() = runTest { + subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES) + subject.clearAllLocalData() + val actual = recipeDao.queryAllRecipes() + assertThat(actual).isEmpty() + } + + @Test + fun `when saveRecipeInfo then saves recipe info`() = runTest { + subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY)) + subject.saveRecipeInfo( + CAKE_RECIPE_ENTITY, + listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY), + listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY) + ) + val actual = recipeDao.queryFullRecipeInfo("1") + assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY) + } + + @Test + fun `when saveRecipeInfo with two then saves second`() = runTest { + subject.saveRecipes(TEST_RECIPE_SUMMARY_ENTITIES) + subject.saveRecipeInfo( + CAKE_RECIPE_ENTITY, + listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY), + listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY), + ) + subject.saveRecipeInfo( + PORRIDGE_RECIPE_ENTITY_FULL, + listOf(PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY, PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY), + listOf(PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY, PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY), + ) + val actual = recipeDao.queryFullRecipeInfo("2") + assertThat(actual).isEqualTo(FULL_PORRIDGE_INFO_ENTITY) + } + + @Test + fun `when saveRecipeInfo twice then overwrites ingredients`() = runTest { + subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY)) + subject.saveRecipeInfo( + CAKE_RECIPE_ENTITY, + listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY), + listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY), + ) + subject.saveRecipeInfo( + CAKE_RECIPE_ENTITY, + listOf(CAKE_BREAD_RECIPE_INGREDIENT_ENTITY), + listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY), + ) + val actual = recipeDao.queryFullRecipeInfo("1")?.recipeIngredients + val expected = listOf(CAKE_BREAD_RECIPE_INGREDIENT_ENTITY.copy(localId = 3)) + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `when saveRecipeInfo twice then overwrites instructions`() = runTest { + subject.saveRecipes(listOf(CAKE_RECIPE_SUMMARY_ENTITY)) + subject.saveRecipeInfo( + CAKE_RECIPE_ENTITY, + listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY), + listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY), + ) + subject.saveRecipeInfo( + CAKE_RECIPE_ENTITY, + listOf(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, CAKE_BREAD_RECIPE_INGREDIENT_ENTITY), + listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY), + ) + val actual = recipeDao.queryFullRecipeInfo("1")?.recipeInstructions + val expected = listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY.copy(localId = 3)) + assertThat(actual).isEqualTo(expected) + } +} \ No newline at end of file diff --git a/database_test/.gitignore b/database_test/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/database_test/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/database_test/build.gradle.kts b/database_test/build.gradle.kts new file mode 100644 index 0000000..57937af --- /dev/null +++ b/database_test/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("gq.kirmanak.mealient.library") +} + +android { + namespace = "gq.kirmanak.mealient.database_test" +} + +dependencies { + implementation(project(":database")) +} \ No newline at end of file diff --git a/database_test/src/main/kotlin/gq/kirmanak/mealient/database/TestData.kt b/database_test/src/main/kotlin/gq/kirmanak/mealient/database/TestData.kt new file mode 100644 index 0000000..fe6e429 --- /dev/null +++ b/database_test/src/main/kotlin/gq/kirmanak/mealient/database/TestData.kt @@ -0,0 +1,128 @@ +package gq.kirmanak.mealient.database + +import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeWithSummaryAndIngredientsAndInstructions +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime + +val CAKE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity( + remoteId = "1", + name = "Cake", + slug = "cake", + description = "A tasty cake", + dateAdded = LocalDate.parse("2021-11-13"), + dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"), + imageId = "cake", + isFavorite = false, +) + +val PORRIDGE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity( + remoteId = "2", + name = "Porridge", + slug = "porridge", + description = "A tasty porridge", + dateAdded = LocalDate.parse("2021-11-12"), + dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), + imageId = "porridge", + isFavorite = false, +) + +val TEST_RECIPE_SUMMARY_ENTITIES = + listOf(CAKE_RECIPE_SUMMARY_ENTITY, PORRIDGE_RECIPE_SUMMARY_ENTITY) + +val MIX_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( + recipeId = "1", + text = "Mix the ingredients", +) + +val BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( + recipeId = "1", + text = "Bake the ingredients", +) + +val CAKE_RECIPE_ENTITY = RecipeEntity( + remoteId = "1", + recipeYield = "4 servings", + disableAmounts = true, +) + +val CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( + recipeId = "1", + note = "2 oz of white sugar", + quantity = 1.0, + unit = null, + food = null, + title = null, +) + +val CAKE_BREAD_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( + recipeId = "1", + note = "2 oz of white bread", + quantity = 1.0, + unit = null, + food = null, + title = null, +) + +val FULL_CAKE_INFO_ENTITY = RecipeWithSummaryAndIngredientsAndInstructions( + recipeEntity = CAKE_RECIPE_ENTITY, + recipeSummaryEntity = CAKE_RECIPE_SUMMARY_ENTITY, + recipeIngredients = listOf( + CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY, + CAKE_BREAD_RECIPE_INGREDIENT_ENTITY, + ), + recipeInstructions = listOf( + MIX_CAKE_RECIPE_INSTRUCTION_ENTITY, + BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY, + ), +) + +val PORRIDGE_RECIPE_ENTITY_FULL = RecipeEntity( + remoteId = "2", + recipeYield = "3 servings", + disableAmounts = true, +) + +val PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( + recipeId = "2", + note = "2 oz of white milk", + quantity = 1.0, + unit = null, + food = null, + title = null, +) + +val PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( + recipeId = "2", + note = "2 oz of white sugar", + quantity = 1.0, + unit = null, + food = null, + title = null, +) + +val PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( + recipeId = "2", + text = "Mix the ingredients" +) + +val PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( + recipeId = "2", + text = "Boil the ingredients" +) + +val FULL_PORRIDGE_INFO_ENTITY = RecipeWithSummaryAndIngredientsAndInstructions( + recipeEntity = PORRIDGE_RECIPE_ENTITY_FULL, + recipeSummaryEntity = PORRIDGE_RECIPE_SUMMARY_ENTITY, + recipeIngredients = listOf( + PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY, + PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY, + ), + recipeInstructions = listOf( + PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY, + PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY, + ) +) diff --git a/datasource/build.gradle.kts b/datasource/build.gradle.kts index eb96003..e882678 100644 --- a/datasource/build.gradle.kts +++ b/datasource/build.gradle.kts @@ -21,7 +21,7 @@ dependencies { kaptTest(libs.google.dagger.hiltAndroidCompiler) testImplementation(libs.google.dagger.hiltAndroidTesting) - implementation(libs.jetbrains.kotlinx.datetime) + api(libs.jetbrains.kotlinx.datetime) implementation(libs.jetbrains.kotlinx.serialization) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeInfo.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/AddRecipeInfo.kt similarity index 91% rename from app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeInfo.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/AddRecipeInfo.kt index bb51ac6..4c84a54 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeInfo.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/AddRecipeInfo.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.add +package gq.kirmanak.mealient.datasource.models data class AddRecipeInfo( val name: String, diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/FullRecipeInfo.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/FullRecipeInfo.kt similarity index 91% rename from app/src/main/java/gq/kirmanak/mealient/data/recipes/network/FullRecipeInfo.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/FullRecipeInfo.kt index eb383da..3e38561 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/FullRecipeInfo.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/FullRecipeInfo.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.recipes.network +package gq.kirmanak.mealient.datasource.models data class FullRecipeInfo( val remoteId: String, diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/FullShoppingListInfo.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/FullShoppingListInfo.kt new file mode 100644 index 0000000..b49bdc4 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/FullShoppingListInfo.kt @@ -0,0 +1,28 @@ +package gq.kirmanak.mealient.datasource.models + +data class FullShoppingListInfo( + val id: String, + val name: String, + val items: List, +) + +data class ShoppingListItemInfo( + val shoppingListId: String, + val id: String, + val checked: Boolean, + val position: Int, + val isFood: Boolean, + val note: String, + val quantity: Double, + val unit: String, + val food: String, + val recipeReferences: List, +) + +data class ShoppingListItemRecipeReferenceInfo( + val recipeId: String, + val recipeQuantity: Double, + val id: String, + val shoppingListId: String, + val recipe: FullRecipeInfo, +) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/share/ParseRecipeURLInfo.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ParseRecipeURLInfo.kt similarity index 64% rename from app/src/main/java/gq/kirmanak/mealient/data/share/ParseRecipeURLInfo.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ParseRecipeURLInfo.kt index 7866216..59358c3 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/share/ParseRecipeURLInfo.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ParseRecipeURLInfo.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.share +package gq.kirmanak.mealient.datasource.models data class ParseRecipeURLInfo( val url: String, diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeSummaryInfo.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/RecipeSummaryInfo.kt similarity index 85% rename from app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeSummaryInfo.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/RecipeSummaryInfo.kt index 9e9cf8f..8a99361 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeSummaryInfo.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/RecipeSummaryInfo.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.recipes.network +package gq.kirmanak.mealient.datasource.models import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ShoppingListsInfo.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ShoppingListsInfo.kt new file mode 100644 index 0000000..bec5bb9 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ShoppingListsInfo.kt @@ -0,0 +1,14 @@ +package gq.kirmanak.mealient.datasource.models + +data class ShoppingListsInfo( + val page: Int, + val perPage: Int, + val totalPages: Int, + val totalItems: Int, + val items: List, +) + +data class ShoppingListInfo( + val name: String, + val id: String, +) \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionInfo.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/VersionInfo.kt similarity index 52% rename from app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionInfo.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/VersionInfo.kt index a1eb010..a746dc2 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionInfo.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/VersionInfo.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.baseurl +package gq.kirmanak.mealient.datasource.models data class VersionInfo( val version: String, diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt index 5204405..945c660 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt @@ -5,6 +5,8 @@ import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1 import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1 import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1 import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 @@ -54,5 +56,12 @@ interface MealieDataSourceV1 { suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String) suspend fun addFavoriteRecipe(userId: String, recipeSlug: String) + suspend fun deleteRecipe(slug: String) + + suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponseV1 + + suspend fun getShoppingList(id: String): GetShoppingListResponseV1 + + suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean) } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt index 7191cbd..1f16b99 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt @@ -9,12 +9,20 @@ import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1 import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1 import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1 import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1 import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import retrofit2.HttpException import java.net.ConnectException import java.net.SocketTimeoutException @@ -134,5 +142,52 @@ class MealieDataSourceV1Impl @Inject constructor( logMethod = { "deleteRecipe" }, logParameters = { "slug = $slug" } ) -} + override suspend fun getShoppingLists( + page: Int, + perPage: Int, + ): GetShoppingListsResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getShoppingLists(page, perPage) }, + logMethod = { "getShoppingLists" }, + logParameters = { "page = $page, perPage = $perPage" } + ) + + override suspend fun getShoppingList( + id: String + ): GetShoppingListResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getShoppingList(id) }, + logMethod = { "getShoppingList" }, + logParameters = { "id = $id" } + ) + + private suspend fun getShoppingListItem( + id: String, + ): JsonElement = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getShoppingListItem(id) }, + logMethod = { "getShoppingListItem" }, + logParameters = { "id = $id" } + ) + + private suspend fun updateShoppingListItem( + id: String, + request: JsonElement, + ) = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.updateShoppingListItem(id, request) }, + logMethod = { "updateShoppingListItem" }, + logParameters = { "id = $id, request = $request" } + ) + + override suspend fun updateIsShoppingListItemChecked( + id: String, + isChecked: Boolean + ) { + // Has to be done in two steps because the API doesn't support updating the checked state + val item = getShoppingListItem(id) + val wasChecked = item.jsonObject.getValue("checked").jsonPrimitive.boolean + if (wasChecked == isChecked) return + val updatedItem = item.jsonObject.toMutableMap().apply { + put("checked", JsonPrimitive(isChecked)) + } + updateShoppingListItem(id, JsonObject(updatedItem)) + } +} diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt index 6321265..bbb7c22 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt @@ -1,6 +1,7 @@ package gq.kirmanak.mealient.datasource.v1 import gq.kirmanak.mealient.datasource.v1.models.* +import kotlinx.serialization.json.JsonElement import retrofit2.http.* interface MealieServiceV1 { @@ -66,4 +67,26 @@ interface MealieServiceV1 { suspend fun deleteRecipe( @Path("slug") slug: String ) + + @GET("/api/groups/shopping/lists") + suspend fun getShoppingLists( + @Query("page") page: Int, + @Query("perPage") perPage: Int, + ): GetShoppingListsResponseV1 + + @GET("/api/groups/shopping/lists/{id}") + suspend fun getShoppingList( + @Path("id") id: String, + ): GetShoppingListResponseV1 + + @GET("/api/groups/shopping/items/{id}") + suspend fun getShoppingListItem( + @Path("id") id: String, + ): JsonElement + + @PUT("/api/groups/shopping/items/{id}") + suspend fun updateShoppingListItem( + @Path("id") id: String, + @Body request: JsonElement, + ) } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeIngredientFoodResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeIngredientFoodResponseV1.kt new file mode 100644 index 0000000..94a1e55 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeIngredientFoodResponseV1.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetRecipeIngredientFoodResponseV1( + @SerialName("name") val name: String = "", +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeIngredientUnitResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeIngredientUnitResponseV1.kt new file mode 100644 index 0000000..626d5c0 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeIngredientUnitResponseV1.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetRecipeIngredientUnitResponseV1( + @SerialName("name") val name: String = "", +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeResponseV1.kt index eb5e690..54e70de 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeResponseV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeResponseV1.kt @@ -10,7 +10,7 @@ data class GetRecipeResponseV1( @SerialName("recipeYield") val recipeYield: String = "", @SerialName("recipeIngredient") val recipeIngredients: List = emptyList(), @SerialName("recipeInstructions") val recipeInstructions: List = emptyList(), - @SerialName("settings") val settings: GetRecipeSettingsResponseV1, + @SerialName("settings") val settings: GetRecipeSettingsResponseV1? = null, ) @Serializable @@ -27,16 +27,6 @@ data class GetRecipeIngredientResponseV1( @SerialName("title") val title: String?, ) -@Serializable -data class GetRecipeIngredientFoodResponseV1( - @SerialName("name") val name: String = "", -) - -@Serializable -data class GetRecipeIngredientUnitResponseV1( - @SerialName("name") val name: String = "", -) - @Serializable data class GetRecipeInstructionResponseV1( @SerialName("text") val text: String, diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListResponseV1.kt new file mode 100644 index 0000000..6f6739e --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListResponseV1.kt @@ -0,0 +1,42 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetShoppingListResponseV1( + @SerialName("id") val id: String, + @SerialName("groupId") val groupId: String, + @SerialName("name") val name: String = "", + @SerialName("listItems") val listItems: List = emptyList(), + @SerialName("recipeReferences") val recipeReferences: List, +) + +@Serializable +data class GetShoppingListItemResponseV1( + @SerialName("shoppingListId") val shoppingListId: String, + @SerialName("id") val id: String, + @SerialName("checked") val checked: Boolean = false, + @SerialName("position") val position: Int = 0, + @SerialName("isFood") val isFood: Boolean = false, + @SerialName("note") val note: String = "", + @SerialName("quantity") val quantity: Double = 0.0, + @SerialName("unit") val unit: GetRecipeIngredientUnitResponseV1? = null, + @SerialName("food") val food: GetRecipeIngredientFoodResponseV1? = null, + @SerialName("recipeReferences") val recipeReferences: List = emptyList(), +) + +@Serializable +data class GetShoppingListItemRecipeReferenceResponseV1( + @SerialName("recipeId") val recipeId: String, + @SerialName("recipeQuantity") val recipeQuantity: Double = 0.0 +) + +@Serializable +data class GetShoppingListItemRecipeReferenceFullResponseV1( + @SerialName("id") val id: String, + @SerialName("shoppingListId") val shoppingListId: String, + @SerialName("recipeId") val recipeId: String, + @SerialName("recipeQuantity") val recipeQuantity: Double = 0.0, + @SerialName("recipe") val recipe: GetRecipeResponseV1, +) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListsResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListsResponseV1.kt new file mode 100644 index 0000000..19bad53 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListsResponseV1.kt @@ -0,0 +1,13 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetShoppingListsResponseV1( + @SerialName("page") val page: Int, + @SerialName("per_page") val perPage: Int, + @SerialName("total") val total: Int, + @SerialName("total_pages") val totalPages: Int, + @SerialName("items") val items: List, +) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListsSummaryResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListsSummaryResponseV1.kt new file mode 100644 index 0000000..addbce9 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetShoppingListsSummaryResponseV1.kt @@ -0,0 +1,10 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetShoppingListsSummaryResponseV1( + @SerialName("id") val id: String, + @SerialName("name") val name: String?, +) \ No newline at end of file diff --git a/datasource_test/.gitignore b/datasource_test/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/datasource_test/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/datasource_test/build.gradle.kts b/datasource_test/build.gradle.kts new file mode 100644 index 0000000..6623270 --- /dev/null +++ b/datasource_test/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("gq.kirmanak.mealient.library") +} + +android { + namespace = "gq.kirmanak.mealient.datasource_test" +} + +dependencies { + implementation(project(":datasource")) +} diff --git a/datasource_test/src/main/kotlin/gq/kirmanak/mealient/datasource_test/TestData.kt b/datasource_test/src/main/kotlin/gq/kirmanak/mealient/datasource_test/TestData.kt new file mode 100644 index 0000000..d0ff5c1 --- /dev/null +++ b/datasource_test/src/main/kotlin/gq/kirmanak/mealient/datasource_test/TestData.kt @@ -0,0 +1,301 @@ +package gq.kirmanak.mealient.datasource_test + +import gq.kirmanak.mealient.datasource.models.AddRecipeInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeIngredientInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeInstructionInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeSettingsInfo +import gq.kirmanak.mealient.datasource.models.FullRecipeInfo +import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo +import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo +import gq.kirmanak.mealient.datasource.models.RecipeSettingsInfo +import gq.kirmanak.mealient.datasource.models.RecipeSummaryInfo +import gq.kirmanak.mealient.datasource.models.VersionInfo +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeSettingsV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeIngredientResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeInstructionResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeIngredientV1 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1 +import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1 +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime + +val RECIPE_SUMMARY_CAKE = RecipeSummaryInfo( + remoteId = "1", + name = "Cake", + slug = "cake", + description = "A tasty cake", + dateAdded = LocalDate.parse("2021-11-13"), + dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"), + imageId = "cake", +) + +val RECIPE_SUMMARY_PORRIDGE_V0 = RecipeSummaryInfo( + remoteId = "2", + name = "Porridge", + slug = "porridge", + description = "A tasty porridge", + dateAdded = LocalDate.parse("2021-11-12"), + dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), + imageId = "porridge", +) + +val RECIPE_SUMMARY_PORRIDGE_V1 = RecipeSummaryInfo( + remoteId = "2", + name = "Porridge", + slug = "porridge", + description = "A tasty porridge", + dateAdded = LocalDate.parse("2021-11-12"), + dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), + imageId = "2", +) + +val TEST_RECIPE_SUMMARIES = listOf(RECIPE_SUMMARY_CAKE, RECIPE_SUMMARY_PORRIDGE_V0) + +val SUGAR_INGREDIENT = RecipeIngredientInfo( + note = "2 oz of white sugar", + quantity = 1.0, + unit = null, + food = null, + title = null, +) + +val BREAD_INGREDIENT = RecipeIngredientInfo( + note = "2 oz of white bread", + quantity = 1.0, + unit = null, + food = null, + title = null, +) + +private val MILK_INGREDIENT = RecipeIngredientInfo( + note = "2 oz of white milk", + quantity = 1.0, + unit = null, + food = null, + title = null, +) + +val MIX_INSTRUCTION = RecipeInstructionInfo( + text = "Mix the ingredients" +) + +private val BAKE_INSTRUCTION = RecipeInstructionInfo( + text = "Bake the ingredients" +) + +private val BOIL_INSTRUCTION = RecipeInstructionInfo( + text = "Boil the ingredients" +) + +val CAKE_FULL_RECIPE_INFO = FullRecipeInfo( + remoteId = "1", + name = "Cake", + recipeYield = "4 servings", + recipeIngredients = listOf(SUGAR_INGREDIENT, BREAD_INGREDIENT), + recipeInstructions = listOf(MIX_INSTRUCTION, BAKE_INSTRUCTION), + settings = RecipeSettingsInfo(disableAmounts = true) +) + +val PORRIDGE_FULL_RECIPE_INFO = FullRecipeInfo( + remoteId = "2", + name = "Porridge", + recipeYield = "3 servings", + recipeIngredients = listOf(SUGAR_INGREDIENT, MILK_INGREDIENT), + recipeInstructions = listOf(MIX_INSTRUCTION, BOIL_INSTRUCTION), + settings = RecipeSettingsInfo(disableAmounts = true) +) + +val SUGAR_ADD_RECIPE_INGREDIENT_INFO = AddRecipeIngredientInfo("2 oz of white sugar") + +val MILK_ADD_RECIPE_INGREDIENT_INFO = AddRecipeIngredientInfo("2 oz of white milk") + +val BOIL_ADD_RECIPE_INSTRUCTION_INFO = AddRecipeInstructionInfo("Boil the ingredients") + +val MIX_ADD_RECIPE_INSTRUCTION_INFO = AddRecipeInstructionInfo("Mix the ingredients") + +val ADD_RECIPE_INFO_SETTINGS = AddRecipeSettingsInfo(disableComments = false, public = true) + +val PORRIDGE_ADD_RECIPE_INFO = AddRecipeInfo( + name = "Porridge", + description = "A tasty porridge", + recipeYield = "3 servings", + recipeIngredient = listOf( + MILK_ADD_RECIPE_INGREDIENT_INFO, + SUGAR_ADD_RECIPE_INGREDIENT_INFO, + ), + recipeInstructions = listOf( + MIX_ADD_RECIPE_INSTRUCTION_INFO, + BOIL_ADD_RECIPE_INSTRUCTION_INFO, + ), + settings = ADD_RECIPE_INFO_SETTINGS, +) + +val PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0 = GetRecipeSummaryResponseV0( + remoteId = 2, + name = "Porridge", + slug = "porridge", + description = "A tasty porridge", + dateAdded = LocalDate.parse("2021-11-12"), + dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), +) + +val PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1 = GetRecipeSummaryResponseV1( + remoteId = "2", + name = "Porridge", + slug = "porridge", + description = "A tasty porridge", + dateAdded = LocalDate.parse("2021-11-12"), + dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), +) + +val VERSION_RESPONSE_V0 = VersionResponseV0("v0.5.6") + +val VERSION_INFO_V0 = VersionInfo("v0.5.6") + +val VERSION_RESPONSE_V1 = VersionResponseV1("v1.0.0-beta05") + +val VERSION_INFO_V1 = VersionInfo("v1.0.0-beta05") + +val MILK_RECIPE_INGREDIENT_RESPONSE_V0 = GetRecipeIngredientResponseV0("2 oz of white milk") + +val SUGAR_RECIPE_INGREDIENT_RESPONSE_V0 = GetRecipeIngredientResponseV0("2 oz of white sugar") + +val MILK_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1( + note = "2 oz of white milk", + quantity = 1.0, + unit = null, + food = null, + title = null, +) + +val SUGAR_RECIPE_INGREDIENT_RESPONSE_V1 = GetRecipeIngredientResponseV1( + note = "2 oz of white sugar", + quantity = 1.0, + unit = null, + food = null, + title = null, +) + +val MILK_RECIPE_INGREDIENT_INFO = RecipeIngredientInfo( + note = "2 oz of white milk", + quantity = 1.0, + unit = null, + food = null, + title = null, +) + +val MIX_RECIPE_INSTRUCTION_RESPONSE_V0 = GetRecipeInstructionResponseV0("Mix the ingredients") + +val BOIL_RECIPE_INSTRUCTION_RESPONSE_V0 = GetRecipeInstructionResponseV0("Boil the ingredients") + +val MIX_RECIPE_INSTRUCTION_RESPONSE_V1 = GetRecipeInstructionResponseV1("Mix the ingredients") + +val BOIL_RECIPE_INSTRUCTION_RESPONSE_V1 = GetRecipeInstructionResponseV1("Boil the ingredients") + +val MIX_RECIPE_INSTRUCTION_INFO = RecipeInstructionInfo("Mix the ingredients") + +val PORRIDGE_RECIPE_RESPONSE_V0 = GetRecipeResponseV0( + remoteId = 2, + name = "Porridge", + recipeYield = "3 servings", + recipeIngredients = listOf( + SUGAR_RECIPE_INGREDIENT_RESPONSE_V0, + MILK_RECIPE_INGREDIENT_RESPONSE_V0, + ), + recipeInstructions = listOf( + MIX_RECIPE_INSTRUCTION_RESPONSE_V0, + BOIL_RECIPE_INSTRUCTION_RESPONSE_V0 + ), +) + +val PORRIDGE_RECIPE_RESPONSE_V1 = GetRecipeResponseV1( + remoteId = "2", + recipeYield = "3 servings", + name = "Porridge", + recipeIngredients = listOf( + SUGAR_RECIPE_INGREDIENT_RESPONSE_V1, + MILK_RECIPE_INGREDIENT_RESPONSE_V1, + ), + recipeInstructions = listOf( + MIX_RECIPE_INSTRUCTION_RESPONSE_V1, + BOIL_RECIPE_INSTRUCTION_RESPONSE_V1 + ), +) + +val MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V0 = AddRecipeInstructionV0("Mix the ingredients") + +val BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V0 = AddRecipeInstructionV0("Boil the ingredients") + +val SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V0 = AddRecipeIngredientV0("2 oz of white sugar") + +val MILK_ADD_RECIPE_INGREDIENT_REQUEST_V0 = AddRecipeIngredientV0("2 oz of white milk") + +val ADD_RECIPE_REQUEST_SETTINGS_V0 = AddRecipeSettingsV0(disableComments = false, public = true) + +val PORRIDGE_ADD_RECIPE_REQUEST_V0 = AddRecipeRequestV0( + name = "Porridge", + description = "A tasty porridge", + recipeYield = "3 servings", + recipeInstructions = listOf( + MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V0, + BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V0, + ), + recipeIngredient = listOf( + MILK_ADD_RECIPE_INGREDIENT_REQUEST_V0, + SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V0, + ), + settings = ADD_RECIPE_REQUEST_SETTINGS_V0 +) + +val MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V1 = AddRecipeInstructionV1( + id = "1", + text = "Mix the ingredients", + ingredientReferences = emptyList() +) + +val BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V1 = AddRecipeInstructionV1( + id = "2", + text = "Boil the ingredients", + ingredientReferences = emptyList() +) + +val SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V1 = AddRecipeIngredientV1( + id = "3", + note = "2 oz of white sugar" +) + +val MILK_ADD_RECIPE_INGREDIENT_REQUEST_V1 = AddRecipeIngredientV1( + id = "4", + note = "2 oz of white milk" +) + +val ADD_RECIPE_REQUEST_SETTINGS_V1 = AddRecipeSettingsV1(disableComments = false, public = true) + +val PORRIDGE_CREATE_RECIPE_REQUEST_V1 = CreateRecipeRequestV1(name = "Porridge") + +val PORRIDGE_UPDATE_RECIPE_REQUEST_V1 = UpdateRecipeRequestV1( + description = "A tasty porridge", + recipeYield = "3 servings", + recipeInstructions = listOf( + MIX_ADD_RECIPE_INSTRUCTION_REQUEST_V1, + BOIL_ADD_RECIPE_INSTRUCTION_REQUEST_V1, + ), + recipeIngredient = listOf( + MILK_ADD_RECIPE_INGREDIENT_REQUEST_V1, + SUGAR_ADD_RECIPE_INGREDIENT_REQUEST_V1, + ), + settings = ADD_RECIPE_REQUEST_SETTINGS_V1 +) diff --git a/datastore_test/.gitignore b/datastore_test/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/datastore_test/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/datastore_test/build.gradle.kts b/datastore_test/build.gradle.kts new file mode 100644 index 0000000..eb97c9c --- /dev/null +++ b/datastore_test/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("gq.kirmanak.mealient.library") +} + +android { + namespace = "gq.kirmanak.mealient.datastore_test" +} + +dependencies { + implementation(project(":datastore")) +} diff --git a/datastore_test/src/main/kotlin/gq/kirmanak/mealient/datastore_test/TestData.kt b/datastore_test/src/main/kotlin/gq/kirmanak/mealient/datastore_test/TestData.kt new file mode 100644 index 0000000..b0ffe52 --- /dev/null +++ b/datastore_test/src/main/kotlin/gq/kirmanak/mealient/datastore_test/TestData.kt @@ -0,0 +1,14 @@ +package gq.kirmanak.mealient.datastore_test + +import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft + +val PORRIDGE_RECIPE_DRAFT = AddRecipeDraft( + recipeName = "Porridge", + recipeDescription = "A tasty porridge", + recipeYield = "3 servings", + recipeInstructions = listOf("Mix the ingredients", "Boil the ingredients"), + recipeIngredients = listOf("2 oz of white milk", "2 oz of white sugar"), + isRecipePublic = true, + areCommentsDisabled = false, +) + diff --git a/features/shopping_lists/.gitignore b/features/shopping_lists/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/features/shopping_lists/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/shopping_lists/build.gradle.kts b/features/shopping_lists/build.gradle.kts new file mode 100644 index 0000000..2cf0885 --- /dev/null +++ b/features/shopping_lists/build.gradle.kts @@ -0,0 +1,45 @@ +@file:Suppress("UnstableApiUsage") + +plugins { + id("gq.kirmanak.mealient.library") + alias(libs.plugins.ksp) + id("gq.kirmanak.mealient.compose") + id("kotlin-kapt") + id("dagger.hilt.android.plugin") +} + +android { + namespace = "gq.kirmanak.mealient.shopping_list" +} + +dependencies { + implementation(project(":architecture")) + implementation(project(":logging")) + implementation(project(":datasource")) + implementation(project(":database")) + implementation(project(":ui")) + implementation(project(":model_mapper")) + + implementation(libs.android.material.material) + implementation(libs.androidx.compose.material) + + implementation(libs.google.dagger.hiltAndroid) + kapt(libs.google.dagger.hiltCompiler) + kaptTest(libs.google.dagger.hiltAndroidCompiler) + testImplementation(libs.google.dagger.hiltAndroidTesting) + + implementation(libs.androidx.hilt.navigationCompose) + + implementation(libs.jetbrains.kotlinx.coroutinesAndroid) + testImplementation(libs.jetbrains.kotlinx.coroutinesTest) + + testImplementation(libs.androidx.test.junit) + + testImplementation(libs.google.truth) + + testImplementation(libs.io.mockk) +} + +kapt { + correctErrorTypes = true +} diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/Theme.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/Theme.kt new file mode 100644 index 0000000..6c77051 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/Theme.kt @@ -0,0 +1,46 @@ +package gq.kirmanak.mealient + +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 +import androidx.compose.ui.unit.dp +import com.google.android.material.color.DynamicColors + +@Composable +fun AppTheme( + isDarkTheme: Boolean = isSystemInDarkTheme(), + isDynamicColor: Boolean = DynamicColors.isDynamicColorAvailable(), + content: @Composable () -> Unit +) { + val colorScheme = when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !isDynamicColor -> { + if (isDarkTheme) darkColorScheme() else lightColorScheme() + } + isDarkTheme -> { + dynamicDarkColorScheme(LocalContext.current) + } + else -> { + dynamicLightColorScheme(LocalContext.current) + } + } + + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} + +object Dimens { + + val Small = 8.dp + + val Medium = 16.dp + + val Large = 24.dp +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ShoppingListsModule.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ShoppingListsModule.kt new file mode 100644 index 0000000..1001dfd --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ShoppingListsModule.kt @@ -0,0 +1,29 @@ +package gq.kirmanak.mealient.shopping_lists + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import gq.kirmanak.mealient.shopping_lists.network.ShoppingListsDataSource +import gq.kirmanak.mealient.shopping_lists.network.ShoppingListsDataSourceImpl +import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo +import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepoImpl +import gq.kirmanak.mealient.shopping_lists.util.LoadingHelperFactory +import gq.kirmanak.mealient.shopping_lists.util.LoadingHelperFactoryImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface ShoppingListsModule { + + @Binds + @Singleton + fun bindShoppingListsDataSource(impl: ShoppingListsDataSourceImpl): ShoppingListsDataSource + + @Binds + @Singleton + fun bindShoppingListsRepo(impl: ShoppingListsRepoImpl): ShoppingListsRepo + + @Binds + fun bindLoadingHelperFactory(impl: LoadingHelperFactoryImpl): LoadingHelperFactory +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSource.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSource.kt new file mode 100644 index 0000000..fa901b5 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSource.kt @@ -0,0 +1,13 @@ +package gq.kirmanak.mealient.shopping_lists.network + +import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListInfo + +interface ShoppingListsDataSource { + + suspend fun getAllShoppingLists(): List + + suspend fun getShoppingList(id: String): FullShoppingListInfo + + suspend fun updateIsShoppingListItemChecked(id: String, checked: Boolean) +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSourceImpl.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSourceImpl.kt new file mode 100644 index 0000000..f61b2a9 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSourceImpl.kt @@ -0,0 +1,30 @@ +package gq.kirmanak.mealient.shopping_lists.network + +import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListInfo +import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 +import gq.kirmanak.mealient.model_mapper.ModelMapper +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ShoppingListsDataSourceImpl @Inject constructor( + private val v1Source: MealieDataSourceV1, + private val modelMapper: ModelMapper, +) : ShoppingListsDataSource { + + override suspend fun getAllShoppingLists(): List { + val response = v1Source.getShoppingLists(1, -1) + return response.items.map { modelMapper.toShoppingListInfo(it) } + } + + override suspend fun getShoppingList( + id: String + ): FullShoppingListInfo = modelMapper.toFullShoppingListInfo(v1Source.getShoppingList(id)) + + override suspend fun updateIsShoppingListItemChecked( + id: String, + checked: Boolean, + ) = v1Source.updateIsShoppingListItemChecked(id, checked) +} + diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsAuthRepo.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsAuthRepo.kt new file mode 100644 index 0000000..45600b4 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsAuthRepo.kt @@ -0,0 +1,8 @@ +package gq.kirmanak.mealient.shopping_lists.repo + +import kotlinx.coroutines.flow.Flow + +interface ShoppingListsAuthRepo { + + val isAuthorizedFlow: Flow +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepo.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepo.kt new file mode 100644 index 0000000..6d3bb86 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepo.kt @@ -0,0 +1,13 @@ +package gq.kirmanak.mealient.shopping_lists.repo + +import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListInfo + +interface ShoppingListsRepo { + + suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean) + + suspend fun getShoppingLists(): List + + suspend fun getShoppingList(id: String): FullShoppingListInfo +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepoImpl.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepoImpl.kt new file mode 100644 index 0000000..434772e --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepoImpl.kt @@ -0,0 +1,30 @@ +package gq.kirmanak.mealient.shopping_lists.repo + +import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListInfo +import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.shopping_lists.network.ShoppingListsDataSource +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ShoppingListsRepoImpl @Inject constructor( + private val dataSource: ShoppingListsDataSource, + private val logger: Logger, +) : ShoppingListsRepo { + + override suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean) { + logger.v { "updateIsShoppingListItemChecked() called with: id = $id, isChecked = $isChecked" } + dataSource.updateIsShoppingListItemChecked(id, isChecked) + } + + override suspend fun getShoppingLists(): List { + logger.v { "getShoppingLists() called" } + return dataSource.getAllShoppingLists() + } + + override suspend fun getShoppingList(id: String): FullShoppingListInfo { + logger.v { "getShoppingListItems() called with: id = $id" } + return dataSource.getShoppingList(id) + } +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/MealientApp.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/MealientApp.kt new file mode 100644 index 0000000..e97514c --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/MealientApp.kt @@ -0,0 +1,18 @@ +package gq.kirmanak.mealient.shopping_lists.ui + +import androidx.compose.runtime.Composable +import com.ramcosta.composedestinations.DestinationsNavHost +import com.ramcosta.composedestinations.rememberNavHostEngine + +@Composable +fun MealientApp() { + val engine = rememberNavHostEngine() + val controller = engine.rememberNavController() + + DestinationsNavHost( + navGraph = NavGraphs.root, + engine = engine, + navController = controller, + startRoute = NavGraphs.root.startRoute, + ) +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListScreen.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListScreen.kt new file mode 100644 index 0000000..3a11c50 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListScreen.kt @@ -0,0 +1,223 @@ +package gq.kirmanak.mealient.shopping_lists.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Divider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import com.ramcosta.composedestinations.annotation.Destination +import gq.kirmanak.mealient.AppTheme +import gq.kirmanak.mealient.Dimens +import gq.kirmanak.mealient.datasource.models.FullRecipeInfo +import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo +import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo +import gq.kirmanak.mealient.datasource.models.RecipeSettingsInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListItemRecipeReferenceInfo +import gq.kirmanak.mealient.shopping_list.R +import gq.kirmanak.mealient.shopping_lists.ui.composables.LazyColumnWithLoadingState +import gq.kirmanak.mealient.shopping_lists.util.data +import gq.kirmanak.mealient.shopping_lists.util.map +import java.text.DecimalFormat + +data class ShoppingListNavArgs( + val shoppingListId: String, +) + +@Destination( + navArgsDelegate = ShoppingListNavArgs::class, +) +@Composable +internal fun ShoppingListScreen( + shoppingListViewModel: ShoppingListViewModel = hiltViewModel(), +) { + val loadingState = shoppingListViewModel.loadingState.collectAsState().value + val defaultEmptyListError = stringResource( + R.string.shopping_list_screen_empty_list, + loadingState.data?.name.orEmpty() + ) + + LazyColumnWithLoadingState( + loadingState = loadingState.map { it.items }, + defaultEmptyListError = defaultEmptyListError, + errorToShowInSnackbar = shoppingListViewModel.errorToShowInSnackbar, + onRefresh = shoppingListViewModel::refreshShoppingList, + onSnackbarShown = shoppingListViewModel::onSnackbarShown, + lazyColumnContent = { items -> + val firstCheckedItemIndex = items.indexOfFirst { it.item.checked } + + itemsIndexed(items) { index, item -> + ShoppingListItem( + shoppingListItem = item.item, + isDisabled = item.isDisabled, + showDivider = index == firstCheckedItemIndex && index != 0, + ) { isChecked -> + shoppingListViewModel.onItemCheckedChange(item.item, isChecked) + } + } + } + ) +} + +@Composable +fun ShoppingListItem( + shoppingListItem: ShoppingListItemInfo, + isDisabled: Boolean, + showDivider: Boolean, + modifier: Modifier = Modifier, + onCheckedChange: (Boolean) -> Unit = {}, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(top = Dimens.Small, end = Dimens.Small, start = Dimens.Small), + ) { + if (showDivider) { + Divider() + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + Checkbox( + checked = shoppingListItem.checked, + onCheckedChange = onCheckedChange, + enabled = !isDisabled, + ) + + val isFood = shoppingListItem.isFood + val quantity = shoppingListItem.quantity + .takeUnless { it == 0.0 } + .takeUnless { it == 1.0 && !isFood } + ?.let { DecimalFormat.getInstance().format(it) } + val text = listOfNotNull( + quantity, + shoppingListItem.unit.takeIf { isFood }, + shoppingListItem.food.takeIf { isFood }, + shoppingListItem.note, + ).filter { it.isNotBlank() }.joinToString(" ") + + Text(text = text) + } + } +} + +@Composable +@Preview +fun PreviewShoppingListItemChecked() { + AppTheme { + ShoppingListItem(shoppingListItem = PreviewData.milk, false, false) + } +} + +@Composable +@Preview +fun PreviewShoppingListItemUnchecked() { + AppTheme { + ShoppingListItem(shoppingListItem = PreviewData.blackTeaBags, false, false) + } +} + +@Composable +@Preview +fun PreviewShoppingListItemCheckedDisabled() { + AppTheme { + ShoppingListItem(shoppingListItem = PreviewData.milk, true, false) + } +} + +@Composable +@Preview +fun PreviewShoppingListItemUncheckedDisabled() { + AppTheme { + ShoppingListItem(shoppingListItem = PreviewData.blackTeaBags, true, false) + } +} + +private object PreviewData { + val teaWithMilkRecipe = FullRecipeInfo( + remoteId = "1", + name = "Tea with milk", + recipeYield = "1 serving", + recipeIngredients = listOf( + RecipeIngredientInfo( + note = "Tea bag", + food = "", + unit = "", + quantity = 1.0, + title = "", + ), + RecipeIngredientInfo( + note = "", + food = "Milk", + unit = "ml", + quantity = 500.0, + title = "", + ), + ), + recipeInstructions = listOf( + RecipeInstructionInfo("Boil water"), + RecipeInstructionInfo("Put tea bag in a cup"), + RecipeInstructionInfo("Pour water into the cup"), + RecipeInstructionInfo("Wait for 5 minutes"), + RecipeInstructionInfo("Remove tea bag"), + RecipeInstructionInfo("Add milk"), + ), + settings = RecipeSettingsInfo( + disableAmounts = false + ), + ) + + val blackTeaBags = ShoppingListItemInfo( + id = "1", + shoppingListId = "1", + checked = false, + position = 0, + isFood = false, + note = "Black tea bags", + quantity = 30.0, + unit = "", + food = "", + recipeReferences = listOf( + ShoppingListItemRecipeReferenceInfo( + shoppingListId = "1", + id = "1", + recipeId = "1", + recipeQuantity = 1.0, + recipe = teaWithMilkRecipe, + ), + ), + ) + + val milk = ShoppingListItemInfo( + id = "2", + shoppingListId = "1", + checked = true, + position = 1, + isFood = true, + note = "Cold", + quantity = 500.0, + unit = "ml", + food = "Milk", + recipeReferences = listOf( + ShoppingListItemRecipeReferenceInfo( + shoppingListId = "1", + id = "2", + recipeId = "1", + recipeQuantity = 500.0, + recipe = teaWithMilkRecipe, + ), + ), + ) +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListScreenState.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListScreenState.kt new file mode 100644 index 0000000..e6df4d3 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListScreenState.kt @@ -0,0 +1,13 @@ +package gq.kirmanak.mealient.shopping_lists.ui + +import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo + +internal data class ShoppingListScreenState( + val name: String, + val items: List, +) + +internal data class ShoppingListItemState( + val item: ShoppingListItemInfo, + val isDisabled: Boolean, +) diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListViewModel.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListViewModel.kt new file mode 100644 index 0000000..d6cc0b6 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListViewModel.kt @@ -0,0 +1,114 @@ +package gq.kirmanak.mealient.shopping_lists.ui + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import gq.kirmanak.mealient.architecture.valueUpdatesOnly +import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel +import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo +import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo +import gq.kirmanak.mealient.shopping_lists.ui.destinations.ShoppingListScreenDestination +import gq.kirmanak.mealient.shopping_lists.util.LoadingHelperFactory +import gq.kirmanak.mealient.shopping_lists.util.LoadingState +import gq.kirmanak.mealient.shopping_lists.util.LoadingStateNoData +import gq.kirmanak.mealient.shopping_lists.util.map +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class ShoppingListViewModel @Inject constructor( + private val shoppingListsRepo: ShoppingListsRepo, + private val logger: Logger, + private val authRepo: ShoppingListsAuthRepo, + loadingHelperFactory: LoadingHelperFactory, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + + private val args: ShoppingListNavArgs = ShoppingListScreenDestination.argsFrom(savedStateHandle) + + private val _disabledItemIds: MutableStateFlow> = MutableStateFlow(mutableSetOf()) + + private val loadingHelper = loadingHelperFactory.create(viewModelScope) { + shoppingListsRepo.getShoppingList(args.shoppingListId) + } + + val loadingState: StateFlow> = combine( + loadingHelper.loadingState, + _disabledItemIds, + ::buildLoadingState, + ).stateIn(viewModelScope, SharingStarted.Eagerly, LoadingStateNoData.InitialLoad) + + private var _errorToShowInSnackbar: Throwable? by mutableStateOf(null) + val errorToShowInSnackbar: Throwable? get() = _errorToShowInSnackbar + + init { + refreshShoppingList() + listenToAuthState() + } + + private fun listenToAuthState() { + logger.v { "listenToAuthState() called" } + viewModelScope.launch { + authRepo.isAuthorizedFlow.valueUpdatesOnly().collect { + logger.d { "Authorization state changed to $it" } + if (it) refreshShoppingList() + } + } + } + + fun refreshShoppingList() { + logger.v { "refreshShoppingList() called" } + viewModelScope.launch { + _errorToShowInSnackbar = loadingHelper.refresh().exceptionOrNull() + } + } + + private fun buildLoadingState( + loadingState: LoadingState, + disabledItemIds: Set, + ): LoadingState { + logger.v { "buildLoadingState() called with: loadingState = $loadingState, disabledItems = $disabledItemIds" } + return loadingState.map { shoppingList -> + val items = shoppingList.items + .sortedBy { it.checked } + .map { ShoppingListItemState(item = it, isDisabled = it.id in disabledItemIds) } + ShoppingListScreenState(name = shoppingList.name, items = items) + } + } + + fun onItemCheckedChange(item: ShoppingListItemInfo, isChecked: Boolean) { + logger.v { "onItemCheckedChange() called with: item = $item, isChecked = $isChecked" } + viewModelScope.launch { + _disabledItemIds.update { it + item.id } + val result = runCatchingExceptCancel { + shoppingListsRepo.updateIsShoppingListItemChecked(item.id, isChecked) + }.onFailure { + logger.e(it) { "Failed to update item's checked state" } + } + _disabledItemIds.update { it - item.id } + _errorToShowInSnackbar = result.exceptionOrNull() + if (result.isSuccess) { + logger.v { "Item's checked state updated" } + refreshShoppingList() + } + } + } + + fun onSnackbarShown() { + logger.v { "onSnackbarShown() called" } + _errorToShowInSnackbar = null + } +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsFragment.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsFragment.kt new file mode 100644 index 0000000..e71b3b3 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsFragment.kt @@ -0,0 +1,47 @@ +package gq.kirmanak.mealient.shopping_lists.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import dagger.hilt.android.AndroidEntryPoint +import gq.kirmanak.mealient.AppTheme +import gq.kirmanak.mealient.ui.ActivityUiStateController +import gq.kirmanak.mealient.ui.CheckableMenuItem +import javax.inject.Inject + +@AndroidEntryPoint +class ShoppingListsFragment : Fragment() { + + @Inject + lateinit var activityUiStateController: ActivityUiStateController + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + AppTheme { + MealientApp() + } + } + } + } + + override fun onResume() { + super.onResume() + activityUiStateController.updateUiState { + it.copy( + navigationVisible = true, + searchVisible = false, + checkedMenuItem = CheckableMenuItem.ShoppingLists, + ) + } + } +} + + diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsScreen.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsScreen.kt new file mode 100644 index 0000000..64f7d1f --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsScreen.kt @@ -0,0 +1,96 @@ +package gq.kirmanak.mealient.shopping_lists.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import gq.kirmanak.mealient.AppTheme +import gq.kirmanak.mealient.Dimens +import gq.kirmanak.mealient.datasource.models.ShoppingListInfo +import gq.kirmanak.mealient.shopping_list.R +import gq.kirmanak.mealient.shopping_lists.ui.composables.LazyColumnWithLoadingState +import gq.kirmanak.mealient.shopping_lists.ui.destinations.ShoppingListScreenDestination + +@RootNavGraph(start = true) +@Destination(start = true) +@Composable +fun ShoppingListsScreen( + navigator: DestinationsNavigator, + shoppingListsViewModel: ShoppingListsViewModel = hiltViewModel(), +) { + val loadingState = shoppingListsViewModel.loadingState.collectAsState() + val errorToShowInSnackbar = shoppingListsViewModel.errorToShowInSnackBar + + LazyColumnWithLoadingState( + loadingState = loadingState.value, + errorToShowInSnackbar = errorToShowInSnackbar, + onSnackbarShown = shoppingListsViewModel::onSnackbarShown, + onRefresh = shoppingListsViewModel::refresh, + defaultEmptyListError = stringResource(R.string.shopping_lists_screen_empty), + lazyColumnContent = { items -> + items(items) { shoppingList -> + ShoppingListCard( + shoppingListEntity = shoppingList, + onItemClick = { clickedEntity -> + val shoppingListId = clickedEntity.id + navigator.navigate(ShoppingListScreenDestination(shoppingListId)) + } + ) + } + } + ) +} + +@Composable +@Preview +private fun PreviewShoppingListCard() { + AppTheme { + ShoppingListCard(shoppingListEntity = ShoppingListInfo("1", "Weekend shopping")) + } +} + +@Composable +private fun ShoppingListCard( + shoppingListEntity: ShoppingListInfo?, + modifier: Modifier = Modifier, + onItemClick: (ShoppingListInfo) -> Unit = {}, +) { + Card( + modifier = modifier + .padding(horizontal = Dimens.Medium, vertical = Dimens.Small) + .fillMaxWidth() + .clickable { shoppingListEntity?.let { onItemClick(it) } }, + ) { + Row( + modifier = Modifier.padding(Dimens.Medium), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_shopping_cart), + contentDescription = stringResource(id = R.string.shopping_lists_screen_cart_icon), + modifier = Modifier.height(Dimens.Large), + ) + Text( + text = shoppingListEntity?.name.orEmpty(), + modifier = Modifier.padding(start = Dimens.Medium), + ) + } + } +} + diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsViewModel.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsViewModel.kt new file mode 100644 index 0000000..6ed10b6 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/ShoppingListsViewModel.kt @@ -0,0 +1,59 @@ +package gq.kirmanak.mealient.shopping_lists.ui + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import gq.kirmanak.mealient.architecture.valueUpdatesOnly +import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsAuthRepo +import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo +import gq.kirmanak.mealient.shopping_lists.util.LoadingHelperFactory +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ShoppingListsViewModel @Inject constructor( + private val logger: Logger, + private val shoppingListsRepo: ShoppingListsRepo, + private val authRepo: ShoppingListsAuthRepo, + loadingHelperFactory: LoadingHelperFactory, +) : ViewModel() { + + private val loadingHelper = loadingHelperFactory.create(viewModelScope) { + shoppingListsRepo.getShoppingLists() + } + val loadingState = loadingHelper.loadingState + + private var _errorToShowInSnackbar by mutableStateOf(null) + val errorToShowInSnackBar: Throwable? get() = _errorToShowInSnackbar + + init { + refresh() + listenToAuthState() + } + + private fun listenToAuthState() { + logger.v { "listenToAuthState() called" } + viewModelScope.launch { + authRepo.isAuthorizedFlow.valueUpdatesOnly().collect { + logger.d { "Authorization state changed to $it" } + if (it) refresh() + } + } + } + + fun refresh() { + logger.v { "refresh() called" } + viewModelScope.launch { + _errorToShowInSnackbar = loadingHelper.refresh().exceptionOrNull() + } + } + + fun onSnackbarShown() { + logger.v { "onSnackbarShown() called" } + _errorToShowInSnackbar = null + } +} diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/CenteredProgressIndicator.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/CenteredProgressIndicator.kt new file mode 100644 index 0000000..65db4dc --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/CenteredProgressIndicator.kt @@ -0,0 +1,30 @@ +package gq.kirmanak.mealient.shopping_lists.ui.composables + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import gq.kirmanak.mealient.AppTheme + +@Composable +fun CenteredProgressIndicator( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } +} + +@Preview +@Composable +fun PreviewCenteredProgressIndicator() { + AppTheme { + CenteredProgressIndicator() + } +} diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/CenteredText.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/CenteredText.kt new file mode 100644 index 0000000..092829a --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/CenteredText.kt @@ -0,0 +1,31 @@ +package gq.kirmanak.mealient.shopping_lists.ui.composables + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import gq.kirmanak.mealient.AppTheme + +@Composable +fun CenteredText( + text: String, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text(text = text) + } +} + +@Preview +@Composable +fun PreviewCenteredText() { + AppTheme { + CenteredText(text = "Hello World") + } +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/EmptyListError.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/EmptyListError.kt new file mode 100644 index 0000000..fff7c0b --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/EmptyListError.kt @@ -0,0 +1,56 @@ +package gq.kirmanak.mealient.shopping_lists.ui.composables + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import gq.kirmanak.mealient.AppTheme +import gq.kirmanak.mealient.Dimens +import gq.kirmanak.mealient.shopping_list.R + +@Composable +fun EmptyListError( + loadError: Throwable?, + onRetry: () -> Unit, + defaultError: String, + modifier: Modifier = Modifier, +) { + val text = loadError?.let { getErrorMessage(it) } ?: defaultError + Box( + modifier = modifier, + ) { + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(top = Dimens.Medium), + text = text, + ) + Button( + modifier = Modifier.padding(top = Dimens.Medium), + onClick = onRetry, + ) { + Text(text = stringResource(id = R.string.shopping_lists_screen_empty_button_refresh)) + } + } + } +} + +@Composable +@Preview +fun PreviewEmptyListError() { + AppTheme { + EmptyListError( + loadError = null, + onRetry = {}, + defaultError = "No items in the list" + ) + } +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/ErrorSnackbar.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/ErrorSnackbar.kt new file mode 100644 index 0000000..347f84e --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/ErrorSnackbar.kt @@ -0,0 +1,29 @@ +package gq.kirmanak.mealient.shopping_lists.ui.composables + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun ErrorSnackbar( + error: Throwable?, + snackbarHostState: SnackbarHostState, + onSnackbarShown: () -> Unit, +) { + if (error == null) { + snackbarHostState.currentSnackbarData?.dismiss() + return + } + + val text = getErrorMessage(error = error) + val scope = rememberCoroutineScope() + + LaunchedEffect(snackbarHostState) { + scope.launch { + snackbarHostState.showSnackbar(message = text) + onSnackbarShown() + } + } +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/GetErrorMessage.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/GetErrorMessage.kt new file mode 100644 index 0000000..03ab1c5 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/GetErrorMessage.kt @@ -0,0 +1,13 @@ +package gq.kirmanak.mealient.shopping_lists.ui.composables + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import gq.kirmanak.mealient.datasource.NetworkError +import gq.kirmanak.mealient.shopping_list.R + +@Composable +fun getErrorMessage(error: Throwable): String = when (error) { + is NetworkError.Unauthorized -> stringResource(R.string.shopping_lists_screen_unauthorized_error) + is NetworkError.NoServerConnection -> stringResource(R.string.shopping_lists_screen_no_connection) + else -> error.message ?: stringResource(R.string.shopping_lists_screen_unknown_error) +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/LazyColumnPullRefresh.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/LazyColumnPullRefresh.kt new file mode 100644 index 0000000..f6bb69c --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/LazyColumnPullRefresh.kt @@ -0,0 +1,33 @@ +package gq.kirmanak.mealient.shopping_lists.ui.composables + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +@OptIn(ExperimentalMaterialApi::class) +fun LazyColumnPullRefresh( + modifier: Modifier = Modifier, + refreshState: PullRefreshState, + isRefreshing: Boolean, + lazyColumnContent: LazyListScope.() -> Unit +) { + Box( + modifier = modifier.pullRefresh(refreshState), + ) { + LazyColumn(modifier = modifier, content = lazyColumnContent) + + PullRefreshIndicator( + modifier = Modifier.align(Alignment.TopCenter), + refreshing = isRefreshing, + state = refreshState + ) + } +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/LazyColumnWithLoadingState.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/LazyColumnWithLoadingState.kt new file mode 100644 index 0000000..5b7d88a --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/LazyColumnWithLoadingState.kt @@ -0,0 +1,79 @@ +package gq.kirmanak.mealient.shopping_lists.ui.composables + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.rememberPullRefreshState +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.Modifier +import gq.kirmanak.mealient.shopping_lists.util.LoadingState +import gq.kirmanak.mealient.shopping_lists.util.LoadingStateNoData +import gq.kirmanak.mealient.shopping_lists.util.data +import gq.kirmanak.mealient.shopping_lists.util.error +import gq.kirmanak.mealient.shopping_lists.util.isLoading +import gq.kirmanak.mealient.shopping_lists.util.isRefreshing + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun LazyColumnWithLoadingState( + loadingState: LoadingState>, + defaultEmptyListError: String, + modifier: Modifier = Modifier, + errorToShowInSnackbar: Throwable? = null, + onSnackbarShown: () -> Unit = {}, + onRefresh: () -> Unit = {}, + lazyColumnContent: LazyListScope.(List) -> Unit = {}, +) { + val refreshState = rememberPullRefreshState( + refreshing = loadingState.isRefreshing, + onRefresh = onRefresh, + ) + val snackbarHostState = remember { SnackbarHostState() } + + Scaffold( + modifier = modifier, + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { paddingValues -> + val innerModifier = Modifier + .padding(paddingValues) + .fillMaxSize() + + val list = loadingState.data ?: emptyList() + + when { + loadingState is LoadingStateNoData.InitialLoad -> { + CenteredProgressIndicator(modifier = innerModifier) + } + + !loadingState.isLoading && list.isEmpty() -> { + EmptyListError( + loadError = loadingState.error, + onRetry = onRefresh, + defaultError = defaultEmptyListError, + modifier = innerModifier, + ) + } + + else -> { + LazyColumnPullRefresh( + modifier = innerModifier, + refreshState = refreshState, + isRefreshing = loadingState.isRefreshing, + lazyColumnContent = { lazyColumnContent(list) }, + ) + + ErrorSnackbar( + error = errorToShowInSnackbar, + snackbarHostState = snackbarHostState, + onSnackbarShown = onSnackbarShown, + ) + } + } + } +} + diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelper.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelper.kt new file mode 100644 index 0000000..cc2fe63 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelper.kt @@ -0,0 +1,10 @@ +package gq.kirmanak.mealient.shopping_lists.util + +import kotlinx.coroutines.flow.StateFlow + +interface LoadingHelper { + + val loadingState: StateFlow> + + suspend fun refresh(): Result +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperFactory.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperFactory.kt new file mode 100644 index 0000000..963e595 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperFactory.kt @@ -0,0 +1,8 @@ +package gq.kirmanak.mealient.shopping_lists.util + +import kotlinx.coroutines.CoroutineScope + +interface LoadingHelperFactory { + + fun create(coroutineScope: CoroutineScope, fetch: suspend () -> T): LoadingHelper +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperFactoryImpl.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperFactoryImpl.kt new file mode 100644 index 0000000..8abaaa6 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperFactoryImpl.kt @@ -0,0 +1,17 @@ +package gq.kirmanak.mealient.shopping_lists.util + +import gq.kirmanak.mealient.logging.Logger +import kotlinx.coroutines.CoroutineScope +import javax.inject.Inject + +// @AssistedFactory does not currently support type parameters in the creator method. +// See https://github.com/google/dagger/issues/2279 +class LoadingHelperFactoryImpl @Inject constructor( + private val logger: Logger +) : LoadingHelperFactory { + + override fun create( + coroutineScope: CoroutineScope, + fetch: suspend () -> T + ): LoadingHelper = LoadingHelperImpl(logger, fetch) +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperImpl.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperImpl.kt new file mode 100644 index 0000000..5be7c98 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingHelperImpl.kt @@ -0,0 +1,42 @@ +package gq.kirmanak.mealient.shopping_lists.util + +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel +import gq.kirmanak.mealient.logging.Logger +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class LoadingHelperImpl( + private val logger: Logger, + private val fetch: suspend () -> T, +) : LoadingHelper { + + private val _loadingState = MutableStateFlow>(LoadingStateNoData.InitialLoad) + override val loadingState: StateFlow> = _loadingState + + override suspend fun refresh(): Result { + logger.v { "refresh() called" } + _loadingState.update { currentState -> + when (currentState) { + is LoadingStateWithData -> LoadingStateWithData.Refreshing(currentState.data) + is LoadingStateNoData -> LoadingStateNoData.InitialLoad + } + } + val result = runCatchingExceptCancel { fetch() } + _loadingState.update { currentState -> + result.fold( + onSuccess = { data -> + LoadingStateWithData.Success(data) + }, + onFailure = { error -> + when (currentState) { + is LoadingStateWithData -> LoadingStateWithData.Success(currentState.data) + is LoadingStateNoData -> LoadingStateNoData.LoadError(error) + } + }, + ) + } + return result + } + +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingState.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingState.kt new file mode 100644 index 0000000..acca373 --- /dev/null +++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/util/LoadingState.kt @@ -0,0 +1,58 @@ +package gq.kirmanak.mealient.shopping_lists.util + +sealed class LoadingState + +sealed class LoadingStateWithData : LoadingState() { + + abstract val data: T + + data class Refreshing(override val data: T) : LoadingStateWithData() + + data class Success(override val data: T) : LoadingStateWithData() + +} + +sealed class LoadingStateNoData : LoadingState() { + + object InitialLoad : LoadingStateNoData() + + data class LoadError(val error: Throwable) : LoadingStateNoData() +} + +val LoadingState.isLoading: Boolean + get() = when (this) { + is LoadingStateNoData.LoadError, + is LoadingStateWithData.Success -> false + + is LoadingStateNoData.InitialLoad, + is LoadingStateWithData.Refreshing -> true + } + +val LoadingState.error: Throwable? + get() = when (this) { + is LoadingStateNoData.LoadError -> error + is LoadingStateNoData.InitialLoad, + is LoadingStateWithData.Refreshing, + is LoadingStateWithData.Success -> null + } + +val LoadingState.data: T? + get() = when (this) { + is LoadingStateWithData -> data + is LoadingStateNoData -> null + } + +val LoadingState.isRefreshing: Boolean + get() = when (this) { + is LoadingStateWithData.Refreshing -> true + is LoadingStateWithData.Success, + is LoadingStateNoData.InitialLoad, + is LoadingStateNoData.LoadError -> false + } + +inline fun LoadingState.map(block: (T) -> E) = when (this) { + is LoadingStateWithData.Success -> LoadingStateWithData.Success(block(data)) + is LoadingStateWithData.Refreshing -> LoadingStateWithData.Refreshing(block(data)) + is LoadingStateNoData.InitialLoad -> LoadingStateNoData.InitialLoad + is LoadingStateNoData.LoadError -> LoadingStateNoData.LoadError(error) +} \ No newline at end of file diff --git a/features/shopping_lists/src/main/res/drawable/ic_shopping_cart.xml b/features/shopping_lists/src/main/res/drawable/ic_shopping_cart.xml new file mode 100644 index 0000000..1408c95 --- /dev/null +++ b/features/shopping_lists/src/main/res/drawable/ic_shopping_cart.xml @@ -0,0 +1,10 @@ + + + diff --git a/features/shopping_lists/src/main/res/values-ru/strings.xml b/features/shopping_lists/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000..35ad873 --- /dev/null +++ b/features/shopping_lists/src/main/res/values-ru/strings.xml @@ -0,0 +1,11 @@ + + + Корзина для покупок + Неизвестная ошибка + %1$s пуст + Списки покупок не найдены + Требуется авторизация + Нет соединения с сервером + Неизвестная ошибка + Попробовать снова + \ No newline at end of file diff --git a/features/shopping_lists/src/main/res/values/strings.xml b/features/shopping_lists/src/main/res/values/strings.xml new file mode 100644 index 0000000..1d6eecb --- /dev/null +++ b/features/shopping_lists/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + + Shopping cart + Unknown error + %1$s is empty + No shopping lists found + Authentication is required + No server connection + Unknown error + Try again + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c86ab80..ea7c68e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # https://maven.google.com/web/index.html?q=com.android.tools.build#com.android.tools.build:gradle androidGradlePlugin = "7.4.2" # https://github.com/JetBrains/kotlin/releases -kotlin = "1.7.20" +kotlin = "1.8.22" # https://developer.android.com/jetpack/androidx/releases/navigation googleNavigation = "2.5.3" # https://dagger.dev/hilt/gradle-setup @@ -74,7 +74,7 @@ chucker = "3.5.2" # https://github.com/google/desugar_jdk_libs/blob/master/CHANGELOG.md desugar = "2.0.3" # https://github.com/google/ksp/releases -kspPlugin = "1.7.20-1.0.7" +kspPlugin = "1.8.22-1.0.11" # https://developer.android.com/jetpack/androidx/releases/sharetarget shareTarget = "1.2.0" # https://github.com/KasperskyLab/Kaspresso/releases @@ -83,18 +83,44 @@ kaspresso = "1.5.1" androidXTest = "1.5.0" # https://developer.android.com/jetpack/androidx/releases/test androidXTestOrchestrator = "1.4.2" +# https://mvnrepository.com/artifact/androidx.compose/compose-bom +composeBom = "2023.06.01" +# https://developer.android.com/jetpack/androidx/releases/compose-kotlin +composeKotlinCompilerExtension = "1.4.8" +# https://google.github.io/accompanist/ +accompanistVersion = "0.30.1" +# https://developer.android.com/jetpack/androidx/releases/compose-material +materialCompose = "1.4.3" +# https://composedestinations.rafaelcosta.xyz/setup +composeDestinations = "1.8.42-beta" +# https://mvnrepository.com/artifact/androidx.hilt/hilt-navigation-compose +hiltNavigationCompose = "1.0.0" [libraries] android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } android-tools-desugar = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar" } android-material-material = { group = "com.google.android.material", name = "material", version.ref = "material" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-ui-toolingPreview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-testJunit = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } + +androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "materialCompose" } + +google-accompanist-themeadapter-material3 = { group = "com.google.accompanist", name = "accompanist-themeadapter-material3", version.ref = "accompanistVersion" } + google-dagger-hiltPlugin = { group = "com.google.dagger", name = "hilt-android-gradle-plugin", version.ref = "hilt" } google-dagger-hiltAndroid = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } google-dagger-hiltCompiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } google-dagger-hiltAndroidCompiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } google-dagger-hiltAndroidTesting = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } +androidx-hilt-navigationCompose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } + google-protobuf-javalite = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protobuf" } google-protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } @@ -126,6 +152,7 @@ androidx-paging-commonKtx = { group = "androidx.paging", name = "paging-common-k androidx-lifecycle-livedataKtx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodelKtx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } androidx-datastore-datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" } @@ -172,6 +199,9 @@ chuckerteam-chucker = { group = "com.github.chuckerteam.chucker", name = "librar kaspersky-kaspresso = { group = "com.kaspersky.android-components", name = "kaspresso", version.ref = "kaspresso" } +composeDestinations-core = { group = "io.github.raamcosta.compose-destinations", name = "core", version.ref = "composeDestinations" } +composeDestinations-ksp = { group = "io.github.raamcosta.compose-destinations", name = "ksp", version.ref = "composeDestinations" } + [plugins] sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" } rootcoverage = { id = "nl.neotech.plugin.rootcoverage", version.ref = "rootCoverage" } diff --git a/model_mapper/.gitignore b/model_mapper/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/model_mapper/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/model_mapper/build.gradle.kts b/model_mapper/build.gradle.kts new file mode 100644 index 0000000..ac0726a --- /dev/null +++ b/model_mapper/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id("gq.kirmanak.mealient.library") + id("kotlin-kapt") + id("dagger.hilt.android.plugin") +} + +android { + namespace = "gq.kirmanak.mealient.model_mapper" +} + +dependencies { + implementation(project(":database")) + testImplementation(project(":database_test")) + implementation(project(":datasource")) + testImplementation(project(":datasource_test")) + implementation(project(":datastore")) + testImplementation(project(":datastore_test")) + testImplementation(project(":testing")) + + implementation(libs.google.dagger.hiltAndroid) + kapt(libs.google.dagger.hiltCompiler) + kaptTest(libs.google.dagger.hiltAndroidCompiler) + testImplementation(libs.google.dagger.hiltAndroidTesting) + + testImplementation(libs.androidx.test.junit) + + testImplementation(libs.google.truth) + + testImplementation(libs.io.mockk) +} diff --git a/model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapper.kt b/model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapper.kt new file mode 100644 index 0000000..caf9f75 --- /dev/null +++ b/model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapper.kt @@ -0,0 +1,130 @@ +package gq.kirmanak.mealient.model_mapper + +import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.datasource.models.AddRecipeInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeIngredientInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeInstructionInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeSettingsInfo +import gq.kirmanak.mealient.datasource.models.FullRecipeInfo +import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo +import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo +import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo +import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo +import gq.kirmanak.mealient.datasource.models.RecipeSettingsInfo +import gq.kirmanak.mealient.datasource.models.RecipeSummaryInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListItemRecipeReferenceInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListsInfo +import gq.kirmanak.mealient.datasource.models.VersionInfo +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeSettingsV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeIngredientResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeInstructionResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0 +import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeIngredientV1 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1 +import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSettingsResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListItemRecipeReferenceFullResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListItemResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsSummaryResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1 +import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft + +interface ModelMapper { + + fun toRecipeEntity(fullRecipeInfo: FullRecipeInfo): RecipeEntity + + fun toRecipeIngredientEntity( + recipeIngredientInfo: RecipeIngredientInfo, remoteId: String + ): RecipeIngredientEntity + + fun toRecipeInstructionEntity( + recipeInstructionInfo: RecipeInstructionInfo, remoteId: String + ): RecipeInstructionEntity + + fun toRecipeSummaryEntity( + recipeSummaryInfo: RecipeSummaryInfo, isFavorite: Boolean + ): RecipeSummaryEntity + + fun toAddRecipeInfo(addRecipeDraft: AddRecipeDraft): AddRecipeInfo + + fun toDraft(addRecipeInfo: AddRecipeInfo): AddRecipeDraft + + fun toVersionInfo(versionResponseV1: VersionResponseV1): VersionInfo + + fun toFullRecipeInfo(getRecipeResponseV1: GetRecipeResponseV1): FullRecipeInfo + + fun toRecipeSettingsInfo(getRecipeSettingsResponseV1: GetRecipeSettingsResponseV1?): RecipeSettingsInfo + + fun toRecipeIngredientInfo(getRecipeIngredientResponseV1: GetRecipeIngredientResponseV1): RecipeIngredientInfo + + fun toRecipeInstructionInfo(getRecipeInstructionResponseV1: GetRecipeInstructionResponseV1): RecipeInstructionInfo + + fun toV1CreateRequest(addRecipeInfo: AddRecipeInfo): CreateRecipeRequestV1 + + fun toV1UpdateRequest(addRecipeInfo: AddRecipeInfo): UpdateRecipeRequestV1 + + fun toV1Settings(addRecipeSettingsInfo: AddRecipeSettingsInfo): AddRecipeSettingsV1 + + fun toV1Ingredient(addRecipeIngredientInfo: AddRecipeIngredientInfo): AddRecipeIngredientV1 + + fun toV1Instruction(addRecipeInstructionInfo: AddRecipeInstructionInfo): AddRecipeInstructionV1 + + fun toV1Request(parseRecipeURLInfo: ParseRecipeURLInfo): ParseRecipeURLRequestV1 + + fun toFullShoppingListInfo(getShoppingListResponseV1: GetShoppingListResponseV1): FullShoppingListInfo + + fun toShoppingListItemInfo( + getShoppingListItemResponseV1: GetShoppingListItemResponseV1, + recipes: Map> + ): ShoppingListItemInfo + + fun toShoppingListItemRecipeReferenceInfo( + getShoppingListItemRecipeReferenceFullResponseV1: GetShoppingListItemRecipeReferenceFullResponseV1 + ): ShoppingListItemRecipeReferenceInfo + + fun toShoppingListsInfo(getShoppingListsResponseV1: GetShoppingListsResponseV1): ShoppingListsInfo + + fun toShoppingListInfo(getShoppingListsSummaryResponseV1: GetShoppingListsSummaryResponseV1): ShoppingListInfo + + fun toRecipeSummaryInfo(getRecipeSummaryResponseV1: GetRecipeSummaryResponseV1): RecipeSummaryInfo + + fun toRecipeSummaryInfo(getRecipeSummaryResponseV0: GetRecipeSummaryResponseV0): RecipeSummaryInfo + + fun toVersionInfo(versionResponseV0: VersionResponseV0): VersionInfo + + fun toFullRecipeInfo(getRecipeResponseV0: GetRecipeResponseV0): FullRecipeInfo + + fun toRecipeIngredientInfo(getRecipeIngredientResponseV0: GetRecipeIngredientResponseV0): RecipeIngredientInfo + + fun toRecipeInstructionInfo(getRecipeInstructionResponseV0: GetRecipeInstructionResponseV0): RecipeInstructionInfo + + fun toV0Request(addRecipeInfo: AddRecipeInfo): AddRecipeRequestV0 + + fun toV0Settings(addRecipeSettingsInfo: AddRecipeSettingsInfo): AddRecipeSettingsV0 + + fun toV0Ingredient(addRecipeIngredientInfo: AddRecipeIngredientInfo): AddRecipeIngredientV0 + + fun toV0Instruction(addRecipeInstructionInfo: AddRecipeInstructionInfo): AddRecipeInstructionV0 + + fun toV0Request(parseRecipeURLInfo: ParseRecipeURLInfo): ParseRecipeURLRequestV0 +} \ No newline at end of file diff --git a/model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapperImpl.kt b/model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapperImpl.kt new file mode 100644 index 0000000..30e34c0 --- /dev/null +++ b/model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapperImpl.kt @@ -0,0 +1,310 @@ +package gq.kirmanak.mealient.model_mapper + +import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.datasource.models.AddRecipeInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeIngredientInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeInstructionInfo +import gq.kirmanak.mealient.datasource.models.AddRecipeSettingsInfo +import gq.kirmanak.mealient.datasource.models.FullRecipeInfo +import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo +import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo +import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo +import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo +import gq.kirmanak.mealient.datasource.models.RecipeSettingsInfo +import gq.kirmanak.mealient.datasource.models.RecipeSummaryInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListItemRecipeReferenceInfo +import gq.kirmanak.mealient.datasource.models.ShoppingListsInfo +import gq.kirmanak.mealient.datasource.models.VersionInfo +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeSettingsV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeIngredientResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeInstructionResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0 +import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeIngredientV1 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1 +import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1 +import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSettingsResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListItemRecipeReferenceFullResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListItemResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsSummaryResponseV1 +import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1 +import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1 +import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ModelMapperImpl @Inject constructor() : ModelMapper { + + + override fun toRecipeEntity(fullRecipeInfo: FullRecipeInfo) = RecipeEntity( + remoteId = fullRecipeInfo.remoteId, + recipeYield = fullRecipeInfo.recipeYield, + disableAmounts = fullRecipeInfo.settings.disableAmounts, + ) + + override fun toRecipeIngredientEntity( + recipeIngredientInfo: RecipeIngredientInfo, remoteId: String + ) = RecipeIngredientEntity( + recipeId = remoteId, + note = recipeIngredientInfo.note, + unit = recipeIngredientInfo.unit, + food = recipeIngredientInfo.food, + quantity = recipeIngredientInfo.quantity, + title = recipeIngredientInfo.title, + ) + + override fun toRecipeInstructionEntity( + recipeInstructionInfo: RecipeInstructionInfo, remoteId: String + ) = RecipeInstructionEntity( + recipeId = remoteId, text = recipeInstructionInfo.text + ) + + override fun toRecipeSummaryEntity(recipeSummaryInfo: RecipeSummaryInfo, isFavorite: Boolean) = + RecipeSummaryEntity( + remoteId = recipeSummaryInfo.remoteId, + name = recipeSummaryInfo.name, + slug = recipeSummaryInfo.slug, + description = recipeSummaryInfo.description, + dateAdded = recipeSummaryInfo.dateAdded, + dateUpdated = recipeSummaryInfo.dateUpdated, + imageId = recipeSummaryInfo.imageId, + isFavorite = isFavorite, + ) + + override fun toAddRecipeInfo(addRecipeDraft: AddRecipeDraft) = AddRecipeInfo( + name = addRecipeDraft.recipeName, + description = addRecipeDraft.recipeDescription, + recipeYield = addRecipeDraft.recipeYield, + recipeIngredient = addRecipeDraft.recipeIngredients.map { AddRecipeIngredientInfo(note = it) }, + recipeInstructions = addRecipeDraft.recipeInstructions.map { AddRecipeInstructionInfo(text = it) }, + settings = AddRecipeSettingsInfo( + public = addRecipeDraft.isRecipePublic, + disableComments = addRecipeDraft.areCommentsDisabled, + ) + ) + + override fun toDraft(addRecipeInfo: AddRecipeInfo): AddRecipeDraft = AddRecipeDraft( + recipeName = addRecipeInfo.name, + recipeDescription = addRecipeInfo.description, + recipeYield = addRecipeInfo.recipeYield, + recipeInstructions = addRecipeInfo.recipeInstructions.map { it.text }, + recipeIngredients = addRecipeInfo.recipeIngredient.map { it.note }, + isRecipePublic = addRecipeInfo.settings.public, + areCommentsDisabled = addRecipeInfo.settings.disableComments, + ) + + + override fun toVersionInfo(versionResponseV1: VersionResponseV1) = + VersionInfo(versionResponseV1.version) + + override fun toFullRecipeInfo(getRecipeResponseV1: GetRecipeResponseV1) = FullRecipeInfo( + remoteId = getRecipeResponseV1.remoteId, + name = getRecipeResponseV1.name, + recipeYield = getRecipeResponseV1.recipeYield, + recipeIngredients = getRecipeResponseV1.recipeIngredients.map { toRecipeIngredientInfo(it) }, + recipeInstructions = getRecipeResponseV1.recipeInstructions.map { toRecipeInstructionInfo(it) }, + settings = toRecipeSettingsInfo(getRecipeResponseV1.settings), + ) + + override fun toRecipeSettingsInfo(getRecipeSettingsResponseV1: GetRecipeSettingsResponseV1?) = + RecipeSettingsInfo( + disableAmounts = getRecipeSettingsResponseV1?.disableAmount ?: true, + ) + + override fun toRecipeIngredientInfo(getRecipeIngredientResponseV1: GetRecipeIngredientResponseV1) = + RecipeIngredientInfo( + note = getRecipeIngredientResponseV1.note, + unit = getRecipeIngredientResponseV1.unit?.name, + food = getRecipeIngredientResponseV1.food?.name, + quantity = getRecipeIngredientResponseV1.quantity, + title = getRecipeIngredientResponseV1.title, + ) + + override fun toRecipeInstructionInfo(getRecipeInstructionResponseV1: GetRecipeInstructionResponseV1) = + RecipeInstructionInfo( + text = getRecipeInstructionResponseV1.text + ) + + override fun toV1CreateRequest(addRecipeInfo: AddRecipeInfo) = CreateRecipeRequestV1( + name = addRecipeInfo.name, + ) + + override fun toV1UpdateRequest(addRecipeInfo: AddRecipeInfo) = UpdateRecipeRequestV1( + description = addRecipeInfo.description, + recipeYield = addRecipeInfo.recipeYield, + recipeIngredient = addRecipeInfo.recipeIngredient.map { toV1Ingredient(it) }, + recipeInstructions = addRecipeInfo.recipeInstructions.map { toV1Instruction(it) }, + settings = toV1Settings(addRecipeInfo.settings), + ) + + override fun toV1Settings(addRecipeSettingsInfo: AddRecipeSettingsInfo) = AddRecipeSettingsV1( + disableComments = addRecipeSettingsInfo.disableComments, + public = addRecipeSettingsInfo.public, + ) + + override fun toV1Ingredient(addRecipeIngredientInfo: AddRecipeIngredientInfo) = + AddRecipeIngredientV1( + id = UUID.randomUUID().toString(), + note = addRecipeIngredientInfo.note, + ) + + override fun toV1Instruction(addRecipeInstructionInfo: AddRecipeInstructionInfo) = + AddRecipeInstructionV1( + id = UUID.randomUUID().toString(), + text = addRecipeInstructionInfo.text, + ingredientReferences = emptyList(), + ) + + override fun toV1Request(parseRecipeURLInfo: ParseRecipeURLInfo) = ParseRecipeURLRequestV1( + url = parseRecipeURLInfo.url, + includeTags = parseRecipeURLInfo.includeTags, + ) + + override fun toFullShoppingListInfo(getShoppingListResponseV1: GetShoppingListResponseV1): FullShoppingListInfo { + val recipes = getShoppingListResponseV1.recipeReferences.groupBy { it.recipeId } + return FullShoppingListInfo( + id = getShoppingListResponseV1.id, + name = getShoppingListResponseV1.name, + items = getShoppingListResponseV1.listItems.map { toShoppingListItemInfo(it, recipes) }, + ) + } + + override fun toShoppingListItemInfo( + getShoppingListItemResponseV1: GetShoppingListItemResponseV1, + recipes: Map> + ): ShoppingListItemInfo = ShoppingListItemInfo( + shoppingListId = getShoppingListItemResponseV1.shoppingListId, + id = getShoppingListItemResponseV1.id, + checked = getShoppingListItemResponseV1.checked, + position = getShoppingListItemResponseV1.position, + isFood = getShoppingListItemResponseV1.isFood, + note = getShoppingListItemResponseV1.note, + quantity = getShoppingListItemResponseV1.quantity, + unit = getShoppingListItemResponseV1.unit?.name.orEmpty(), + food = getShoppingListItemResponseV1.food?.name.orEmpty(), + recipeReferences = getShoppingListItemResponseV1.recipeReferences.map { it.recipeId } + .mapNotNull { recipes[it] }.flatten().map { toShoppingListItemRecipeReferenceInfo(it) }, + ) + + override fun toShoppingListItemRecipeReferenceInfo( + getShoppingListItemRecipeReferenceFullResponseV1: GetShoppingListItemRecipeReferenceFullResponseV1 + ) = ShoppingListItemRecipeReferenceInfo( + recipeId = getShoppingListItemRecipeReferenceFullResponseV1.recipeId, + recipeQuantity = getShoppingListItemRecipeReferenceFullResponseV1.recipeQuantity, + id = getShoppingListItemRecipeReferenceFullResponseV1.id, + shoppingListId = getShoppingListItemRecipeReferenceFullResponseV1.shoppingListId, + recipe = toFullRecipeInfo(getShoppingListItemRecipeReferenceFullResponseV1.recipe), + ) + + override fun toShoppingListsInfo(getShoppingListsResponseV1: GetShoppingListsResponseV1) = + ShoppingListsInfo( + page = getShoppingListsResponseV1.page, + perPage = getShoppingListsResponseV1.perPage, + totalPages = getShoppingListsResponseV1.totalPages, + totalItems = getShoppingListsResponseV1.total, + items = getShoppingListsResponseV1.items.map { toShoppingListInfo(it) }, + ) + + override fun toShoppingListInfo(getShoppingListsSummaryResponseV1: GetShoppingListsSummaryResponseV1) = + ShoppingListInfo( + name = getShoppingListsSummaryResponseV1.name.orEmpty(), + id = getShoppingListsSummaryResponseV1.id, + ) + + override fun toRecipeSummaryInfo(getRecipeSummaryResponseV1: GetRecipeSummaryResponseV1) = + RecipeSummaryInfo( + remoteId = getRecipeSummaryResponseV1.remoteId, + name = getRecipeSummaryResponseV1.name, + slug = getRecipeSummaryResponseV1.slug, + description = getRecipeSummaryResponseV1.description, + dateAdded = getRecipeSummaryResponseV1.dateAdded, + dateUpdated = getRecipeSummaryResponseV1.dateUpdated, + imageId = getRecipeSummaryResponseV1.remoteId, + ) + + + override fun toRecipeSummaryInfo(getRecipeSummaryResponseV0: GetRecipeSummaryResponseV0) = + RecipeSummaryInfo( + remoteId = getRecipeSummaryResponseV0.remoteId.toString(), + name = getRecipeSummaryResponseV0.name, + slug = getRecipeSummaryResponseV0.slug, + description = getRecipeSummaryResponseV0.description, + dateAdded = getRecipeSummaryResponseV0.dateAdded, + dateUpdated = getRecipeSummaryResponseV0.dateUpdated, + imageId = getRecipeSummaryResponseV0.slug, + ) + + override fun toVersionInfo(versionResponseV0: VersionResponseV0) = + VersionInfo(versionResponseV0.version) + + override fun toFullRecipeInfo(getRecipeResponseV0: GetRecipeResponseV0) = FullRecipeInfo( + remoteId = getRecipeResponseV0.remoteId.toString(), + name = getRecipeResponseV0.name, + recipeYield = getRecipeResponseV0.recipeYield, + recipeIngredients = getRecipeResponseV0.recipeIngredients.map { toRecipeIngredientInfo(it) }, + recipeInstructions = getRecipeResponseV0.recipeInstructions.map { toRecipeInstructionInfo(it) }, + settings = RecipeSettingsInfo(disableAmounts = true) + ) + + override fun toRecipeIngredientInfo(getRecipeIngredientResponseV0: GetRecipeIngredientResponseV0) = + RecipeIngredientInfo( + note = getRecipeIngredientResponseV0.note, + unit = null, + food = null, + quantity = 1.0, + title = null, + ) + + override fun toRecipeInstructionInfo(getRecipeInstructionResponseV0: GetRecipeInstructionResponseV0) = + RecipeInstructionInfo( + text = getRecipeInstructionResponseV0.text + ) + + override fun toV0Request(addRecipeInfo: AddRecipeInfo) = AddRecipeRequestV0( + name = addRecipeInfo.name, + description = addRecipeInfo.description, + recipeYield = addRecipeInfo.recipeYield, + recipeIngredient = addRecipeInfo.recipeIngredient.map { toV0Ingredient(it) }, + recipeInstructions = addRecipeInfo.recipeInstructions.map { toV0Instruction(it) }, + settings = toV0Settings(addRecipeInfo.settings), + ) + + override fun toV0Settings(addRecipeSettingsInfo: AddRecipeSettingsInfo) = AddRecipeSettingsV0( + disableComments = addRecipeSettingsInfo.disableComments, + public = addRecipeSettingsInfo.public, + ) + + override fun toV0Ingredient(addRecipeIngredientInfo: AddRecipeIngredientInfo) = + AddRecipeIngredientV0( + note = addRecipeIngredientInfo.note, + ) + + override fun toV0Instruction(addRecipeInstructionInfo: AddRecipeInstructionInfo) = + AddRecipeInstructionV0( + text = addRecipeInstructionInfo.text, + ) + + override fun toV0Request(parseRecipeURLInfo: ParseRecipeURLInfo) = ParseRecipeURLRequestV0( + url = parseRecipeURLInfo.url, + ) +} \ No newline at end of file diff --git a/model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapperModule.kt b/model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapperModule.kt new file mode 100644 index 0000000..15e3e73 --- /dev/null +++ b/model_mapper/src/main/kotlin/gq/kirmanak/mealient/model_mapper/ModelMapperModule.kt @@ -0,0 +1,16 @@ +package gq.kirmanak.mealient.model_mapper + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface ModelMapperModule { + + @Binds + @Singleton + fun bindModelMapper(impl: ModelMapperImpl): ModelMapper +} \ No newline at end of file diff --git a/model_mapper/src/test/kotlin/gq/kirmanak/mealient/model_mapper/ModelMappingsTest.kt b/model_mapper/src/test/kotlin/gq/kirmanak/mealient/model_mapper/ModelMappingsTest.kt new file mode 100644 index 0000000..a6713f0 --- /dev/null +++ b/model_mapper/src/test/kotlin/gq/kirmanak/mealient/model_mapper/ModelMappingsTest.kt @@ -0,0 +1,157 @@ +package gq.kirmanak.mealient.model_mapper + +import com.google.common.truth.Truth.assertThat +import gq.kirmanak.mealient.database.CAKE_RECIPE_ENTITY +import gq.kirmanak.mealient.database.CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY +import gq.kirmanak.mealient.database.MIX_CAKE_RECIPE_INSTRUCTION_ENTITY +import gq.kirmanak.mealient.database.PORRIDGE_RECIPE_SUMMARY_ENTITY +import gq.kirmanak.mealient.datasource_test.CAKE_FULL_RECIPE_INFO +import gq.kirmanak.mealient.datasource_test.MILK_RECIPE_INGREDIENT_INFO +import gq.kirmanak.mealient.datasource_test.MILK_RECIPE_INGREDIENT_RESPONSE_V0 +import gq.kirmanak.mealient.datasource_test.MILK_RECIPE_INGREDIENT_RESPONSE_V1 +import gq.kirmanak.mealient.datasource_test.MIX_INSTRUCTION +import gq.kirmanak.mealient.datasource_test.MIX_RECIPE_INSTRUCTION_INFO +import gq.kirmanak.mealient.datasource_test.MIX_RECIPE_INSTRUCTION_RESPONSE_V0 +import gq.kirmanak.mealient.datasource_test.MIX_RECIPE_INSTRUCTION_RESPONSE_V1 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_INFO +import gq.kirmanak.mealient.datasource_test.PORRIDGE_ADD_RECIPE_REQUEST_V0 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_CREATE_RECIPE_REQUEST_V1 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_FULL_RECIPE_INFO +import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_RESPONSE_V0 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_RESPONSE_V1 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1 +import gq.kirmanak.mealient.datasource_test.PORRIDGE_UPDATE_RECIPE_REQUEST_V1 +import gq.kirmanak.mealient.datasource_test.RECIPE_SUMMARY_PORRIDGE_V0 +import gq.kirmanak.mealient.datasource_test.RECIPE_SUMMARY_PORRIDGE_V1 +import gq.kirmanak.mealient.datasource_test.SUGAR_INGREDIENT +import gq.kirmanak.mealient.datasource_test.VERSION_INFO_V0 +import gq.kirmanak.mealient.datasource_test.VERSION_INFO_V1 +import gq.kirmanak.mealient.datasource_test.VERSION_RESPONSE_V0 +import gq.kirmanak.mealient.datasource_test.VERSION_RESPONSE_V1 +import gq.kirmanak.mealient.datastore_test.PORRIDGE_RECIPE_DRAFT +import gq.kirmanak.mealient.test.BaseUnitTest +import org.junit.Before +import org.junit.Test + +class ModelMappingsTest : BaseUnitTest() { + + private lateinit var subject: ModelMapper + + @Before + override fun setUp() { + super.setUp() + subject = ModelMapperImpl() + } + + @Test + fun `when toAddRecipeRequest then fills fields correctly`() { + assertThat(subject.toAddRecipeInfo(PORRIDGE_RECIPE_DRAFT)).isEqualTo( + PORRIDGE_ADD_RECIPE_INFO + ) + } + + @Test + fun `when toDraft then fills fields correctly`() { + assertThat(subject.toDraft(PORRIDGE_ADD_RECIPE_INFO)).isEqualTo(PORRIDGE_RECIPE_DRAFT) + } + + @Test + fun `when full recipe info to entity expect correct entity`() { + assertThat(subject.toRecipeEntity(CAKE_FULL_RECIPE_INFO)).isEqualTo(CAKE_RECIPE_ENTITY) + } + + @Test + fun `when ingredient info to entity expect correct entity`() { + val actual = subject.toRecipeIngredientEntity(SUGAR_INGREDIENT, "1") + assertThat(actual).isEqualTo(CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY) + } + + @Test + fun `when instruction info to entity expect correct entity`() { + val actual = subject.toRecipeInstructionEntity(MIX_INSTRUCTION, "1") + assertThat(actual).isEqualTo(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY) + } + + @Test + fun `when summary v0 to info expect correct info`() { + val actual = subject.toRecipeSummaryInfo(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V0) + assertThat(actual).isEqualTo(RECIPE_SUMMARY_PORRIDGE_V0) + } + + @Test + fun `when summary v1 to info expect correct info`() { + val actual = subject.toRecipeSummaryInfo(PORRIDGE_RECIPE_SUMMARY_RESPONSE_V1) + assertThat(actual).isEqualTo(RECIPE_SUMMARY_PORRIDGE_V1) + } + + @Test + fun `when summary info to entity expect correct entity`() { + val actual = subject.toRecipeSummaryEntity(RECIPE_SUMMARY_PORRIDGE_V0, isFavorite = false) + assertThat(actual).isEqualTo(PORRIDGE_RECIPE_SUMMARY_ENTITY) + } + + @Test + fun `when version response v0 to info expect correct info`() { + assertThat(subject.toVersionInfo(VERSION_RESPONSE_V0)).isEqualTo(VERSION_INFO_V0) + } + + @Test + fun `when version response v1 to info expect correct info`() { + assertThat(subject.toVersionInfo(VERSION_RESPONSE_V1)).isEqualTo(VERSION_INFO_V1) + } + + @Test + fun `when recipe ingredient response v0 to info expect correct info`() { + val actual = subject.toRecipeIngredientInfo(MILK_RECIPE_INGREDIENT_RESPONSE_V0) + assertThat(actual).isEqualTo(MILK_RECIPE_INGREDIENT_INFO) + } + + @Test + fun `when recipe ingredient response v1 to info expect correct info`() { + val actual = subject.toRecipeIngredientInfo(MILK_RECIPE_INGREDIENT_RESPONSE_V1) + assertThat(actual).isEqualTo(MILK_RECIPE_INGREDIENT_INFO) + } + + @Test + fun `when recipe instruction response v0 to info expect correct info`() { + val actual = subject.toRecipeInstructionInfo(MIX_RECIPE_INSTRUCTION_RESPONSE_V0) + assertThat(actual).isEqualTo(MIX_RECIPE_INSTRUCTION_INFO) + } + + @Test + fun `when recipe instruction response v1 to info expect correct info`() { + val actual = subject.toRecipeInstructionInfo(MIX_RECIPE_INSTRUCTION_RESPONSE_V1) + assertThat(actual).isEqualTo(MIX_RECIPE_INSTRUCTION_INFO) + } + + @Test + fun `when recipe response v0 to info expect correct info`() { + val actual = subject.toFullRecipeInfo(PORRIDGE_RECIPE_RESPONSE_V0) + assertThat(actual).isEqualTo(PORRIDGE_FULL_RECIPE_INFO) + } + + @Test + fun `when recipe response v1 to info expect correct info`() { + val actual = subject.toFullRecipeInfo(PORRIDGE_RECIPE_RESPONSE_V1) + assertThat(actual).isEqualTo(PORRIDGE_FULL_RECIPE_INFO) + } + + @Test + fun `when add recipe info to request v0 expect correct request`() { + val actual = subject.toV0Request(PORRIDGE_ADD_RECIPE_INFO) + assertThat(actual).isEqualTo(PORRIDGE_ADD_RECIPE_REQUEST_V0) + } + + @Test + fun `when add recipe info to create request v1 expect correct request`() { + val actual = subject.toV1CreateRequest(PORRIDGE_ADD_RECIPE_INFO) + assertThat(actual).isEqualTo(PORRIDGE_CREATE_RECIPE_REQUEST_V1) + } + + @Test + fun `when add recipe info to update request v1 expect correct request`() { + val actual = subject.toV1UpdateRequest(PORRIDGE_ADD_RECIPE_INFO) + assertThat(actual).isEqualTo(PORRIDGE_UPDATE_RECIPE_REQUEST_V1) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 9123044..1f28dcd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,7 +22,13 @@ rootProject.name = "Mealient" include(":app") include(":architecture") include(":database") +include(":database_test") include(":datastore") +include(":datastore_test") include(":logging") include(":datasource") +include(":datasource_test") include(":testing") +include(":ui") +include(":model_mapper") +include(":features:shopping_lists") diff --git a/template_module/.gitignore b/template_module/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/template_module/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/template_module/build.gradle.kts b/template_module/build.gradle.kts new file mode 100644 index 0000000..b36c99b --- /dev/null +++ b/template_module/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("gq.kirmanak.mealient.library") +} + +android { + namespace = "gq.kirmanak.mealient.MODULE_NAME" +} + +dependencies { +} diff --git a/testing/src/main/kotlin/gq/kirmanak/mealient/test/FakeLogger.kt b/testing/src/main/kotlin/gq/kirmanak/mealient/test/FakeLogger.kt index 1fd425b..dd67afb 100644 --- a/testing/src/main/kotlin/gq/kirmanak/mealient/test/FakeLogger.kt +++ b/testing/src/main/kotlin/gq/kirmanak/mealient/test/FakeLogger.kt @@ -2,8 +2,11 @@ package gq.kirmanak.mealient.test import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.MessageSupplier +import javax.inject.Inject +import javax.inject.Singleton -class FakeLogger : Logger { +@Singleton +class FakeLogger @Inject constructor() : Logger { override fun v(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) { print("V", throwable, messageSupplier) } diff --git a/testing/src/main/kotlin/gq/kirmanak/mealient/test/FakeLoggingModule.kt b/testing/src/main/kotlin/gq/kirmanak/mealient/test/FakeLoggingModule.kt new file mode 100644 index 0000000..98d97f3 --- /dev/null +++ b/testing/src/main/kotlin/gq/kirmanak/mealient/test/FakeLoggingModule.kt @@ -0,0 +1,21 @@ +package gq.kirmanak.mealient.test + +import dagger.Binds +import dagger.Module +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.logging.LoggingModule +import javax.inject.Singleton + +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [LoggingModule::class] +) +interface FakeLoggingModule { + + @Binds + @Singleton + fun bindFakeLogger(impl: FakeLogger): Logger +} \ No newline at end of file diff --git a/testing/src/main/kotlin/gq/kirmanak/mealient/test/HiltRobolectricTest.kt b/testing/src/main/kotlin/gq/kirmanak/mealient/test/HiltRobolectricTest.kt index 63aee1f..05e3cfb 100644 --- a/testing/src/main/kotlin/gq/kirmanak/mealient/test/HiltRobolectricTest.kt +++ b/testing/src/main/kotlin/gq/kirmanak/mealient/test/HiltRobolectricTest.kt @@ -8,6 +8,7 @@ import org.junit.Before import org.junit.Rule import org.junit.runner.RunWith import org.robolectric.annotation.Config +import javax.inject.Inject @RunWith(AndroidJUnit4::class) @Config(application = HiltTestApplication::class, manifest = Config.NONE) @@ -16,7 +17,8 @@ abstract class HiltRobolectricTest { @get:Rule var hiltRule = HiltAndroidRule(this) - protected val logger: Logger = FakeLogger() + @Inject + lateinit var logger: Logger @Before fun inject() { diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts new file mode 100644 index 0000000..176c335 --- /dev/null +++ b/ui/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("gq.kirmanak.mealient.library") + id("kotlin-kapt") + id("dagger.hilt.android.plugin") +} + +android { + namespace = "gq.kirmanak.mealient.ui" +} + +dependencies { + implementation(libs.google.dagger.hiltAndroid) + kapt(libs.google.dagger.hiltCompiler) + kaptTest(libs.google.dagger.hiltAndroidCompiler) + testImplementation(libs.google.dagger.hiltAndroidTesting) + + testImplementation(libs.androidx.test.junit) + + testImplementation(libs.google.truth) + + testImplementation(libs.io.mockk) +} \ No newline at end of file diff --git a/ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiState.kt b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiState.kt new file mode 100644 index 0000000..ea10c97 --- /dev/null +++ b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiState.kt @@ -0,0 +1,21 @@ +package gq.kirmanak.mealient.ui + +data class ActivityUiState( + val isAuthorized: Boolean = false, + val navigationVisible: Boolean = false, + val searchVisible: Boolean = false, + val checkedMenuItem: CheckableMenuItem? = null, + val v1MenuItemsVisible: Boolean = false, +) { + val canShowLogin: Boolean get() = !isAuthorized + + val canShowLogout: Boolean get() = isAuthorized +} + +enum class CheckableMenuItem { + ShoppingLists, + RecipesList, + AddRecipe, + ChangeUrl, + Login +} diff --git a/ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiStateController.kt b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiStateController.kt new file mode 100644 index 0000000..6bd7a28 --- /dev/null +++ b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiStateController.kt @@ -0,0 +1,12 @@ +package gq.kirmanak.mealient.ui + +import kotlinx.coroutines.flow.StateFlow + +interface ActivityUiStateController { + + fun updateUiState(update: (ActivityUiState) -> ActivityUiState) + + fun getUiState(): ActivityUiState + + fun getUiStateFlow(): StateFlow +} \ No newline at end of file diff --git a/ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiStateControllerImpl.kt b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiStateControllerImpl.kt new file mode 100644 index 0000000..1d6ea11 --- /dev/null +++ b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/ActivityUiStateControllerImpl.kt @@ -0,0 +1,21 @@ +package gq.kirmanak.mealient.ui + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.getAndUpdate +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ActivityUiStateControllerImpl @Inject constructor() : ActivityUiStateController { + private val uiState = MutableStateFlow(ActivityUiState()) + + override fun updateUiState(update: (ActivityUiState) -> ActivityUiState) { + uiState.getAndUpdate(update) + } + + override fun getUiState(): ActivityUiState = uiState.value + + override fun getUiStateFlow(): StateFlow = uiState.asStateFlow() +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/OperationUiState.kt b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/OperationUiState.kt similarity index 100% rename from app/src/main/java/gq/kirmanak/mealient/ui/OperationUiState.kt rename to ui/src/main/kotlin/gq/kirmanak/mealient/ui/OperationUiState.kt diff --git a/ui/src/main/kotlin/gq/kirmanak/mealient/ui/UiModule.kt b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/UiModule.kt new file mode 100644 index 0000000..21bc0f8 --- /dev/null +++ b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/UiModule.kt @@ -0,0 +1,16 @@ +package gq.kirmanak.mealient.ui + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface UiModule { + + @Binds + @Singleton + fun bindActivityUiStateController(impl: ActivityUiStateControllerImpl): ActivityUiStateController +} \ No newline at end of file