diff --git a/README.md b/README.md index 2b6f720..0a6c351 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # Mealient -## DISCLAIMER +## DISCLAIMERS This project is developed independently from the core Mealie project. It is NOT associated with the core Mealie developers. Any issues must be reported to the Mealient repository, NOT the Mealie repository. +Also, this is a fork of the original Mealient project. All credit goes to Kirill Kamakin on GitHub for the original project. + ## What is this? An unofficial Android client for [Mealie](https://github.com/mealie-recipes/mealie/). It enables you diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7159bdc..ed0d99f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ plugins { android { defaultConfig { applicationId = "com.atridad.mealient" - versionCode = 37 - versionName = "0.5.0" + versionCode = 38 + versionName = "0.5.1" testInstrumentationRunner = "com.atridad.mealient.MealientTestRunner" testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true") resourceConfigurations += listOf("en", "es", "ru", "fr", "nl", "pt", "de") diff --git a/app/release/Mealient.apk b/app/release/Mealient.apk deleted file mode 100644 index c8b2084..0000000 Binary files a/app/release/Mealient.apk and /dev/null differ diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index 38199c3..5d81bdd 100644 Binary files a/app/release/baselineProfiles/0/app-release.dm and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index dc2a87e..20db0d0 100644 Binary files a/app/release/baselineProfiles/1/app-release.dm and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/release/com.atridad.mealient_0.5.1.apk b/app/release/com.atridad.mealient_0.5.1.apk new file mode 100644 index 0000000..f1a088d Binary files /dev/null and b/app/release/com.atridad.mealient_0.5.1.apk differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 34c3c8a..d18c42f 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 37, - "versionName": "0.5.0", + "versionCode": 38, + "versionName": "0.5.1", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/java/com/atridad/mealient/data/network/MealieDataSourceWrapper.kt b/app/src/main/java/com/atridad/mealient/data/network/MealieDataSourceWrapper.kt index 2e6f3af..5ec1ce4 100644 --- a/app/src/main/java/com/atridad/mealient/data/network/MealieDataSourceWrapper.kt +++ b/app/src/main/java/com/atridad/mealient/data/network/MealieDataSourceWrapper.kt @@ -11,9 +11,11 @@ import com.atridad.mealient.datasource.models.ParseRecipeURLRequest import com.atridad.mealient.model_mapper.ModelMapper import javax.inject.Inject -class MealieDataSourceWrapper @Inject constructor( - private val dataSource: MealieDataSource, - private val modelMapper: ModelMapper, +class MealieDataSourceWrapper +@Inject +constructor( + private val dataSource: MealieDataSource, + private val modelMapper: ModelMapper, ) : AddRecipeDataSource, RecipeDataSource, ParseRecipeDataSource { override suspend fun addRecipe(recipe: AddRecipeInfo): String { @@ -23,10 +25,11 @@ class MealieDataSourceWrapper @Inject constructor( } override suspend fun requestRecipes( - start: Int, - limit: Int, + start: Int, + limit: Int, ): List { - // Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3 + // 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 return dataSource.requestRecipes(page, limit) } @@ -40,11 +43,31 @@ class MealieDataSourceWrapper @Inject constructor( } override suspend fun getFavoriteRecipes(): List { - return dataSource.requestUserInfo().favoriteRecipes + val userInfo = dataSource.requestUserInfo() + + // Use the correct favorites endpoint that actually works + return try { + val favoritesResponse = dataSource.getUserFavoritesAlternative(userInfo.id) + val favoriteRecipeIds = + favoritesResponse.ratings.filter { it.isFavorite }.map { it.recipeId } + + // Get all recipes to create UUID-to-slug mapping + val allRecipes = dataSource.requestRecipes(1, -1) // Get all recipes + val uuidToSlugMap = allRecipes.associate { it.remoteId to it.slug } + + // Map favorite UUIDs to slugs + val favoriteSlugs = favoriteRecipeIds.mapNotNull { uuid -> uuidToSlugMap[uuid] } + + favoriteSlugs + } catch (e: Exception) { + emptyList() + } } override suspend fun updateIsRecipeFavorite(recipeSlug: String, isFavorite: Boolean) { - val userId = dataSource.requestUserInfo().id + val userInfo = dataSource.requestUserInfo() + val userId = userInfo.id + if (isFavorite) { dataSource.addFavoriteRecipe(userId, recipeSlug) } else { @@ -55,4 +78,4 @@ class MealieDataSourceWrapper @Inject constructor( override suspend fun deleteRecipe(recipeSlug: String) { dataSource.deleteRecipe(recipeSlug) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/atridad/mealient/data/recipes/impl/RecipesRemoteMediator.kt b/app/src/main/java/com/atridad/mealient/data/recipes/impl/RecipesRemoteMediator.kt index 7bfcd7c..ddc811e 100644 --- a/app/src/main/java/com/atridad/mealient/data/recipes/impl/RecipesRemoteMediator.kt +++ b/app/src/main/java/com/atridad/mealient/data/recipes/impl/RecipesRemoteMediator.kt @@ -11,30 +11,34 @@ import com.atridad.mealient.database.recipe.entity.RecipeSummaryEntity import com.atridad.mealient.datasource.runCatchingExceptCancel import com.atridad.mealient.logging.Logger import com.atridad.mealient.model_mapper.ModelMapper +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext -import javax.inject.Inject -import javax.inject.Singleton @OptIn(ExperimentalPagingApi::class) @Singleton -class RecipesRemoteMediator @Inject constructor( - private val storage: RecipeStorage, - private val network: RecipeDataSource, - private val pagingSourceFactory: RecipePagingSourceFactory, - private val logger: Logger, - private val modelMapper: ModelMapper, - private val dispatchers: AppDispatchers, +class RecipesRemoteMediator +@Inject +constructor( + private val storage: RecipeStorage, + private val network: RecipeDataSource, + private val pagingSourceFactory: RecipePagingSourceFactory, + private val logger: Logger, + private val modelMapper: ModelMapper, + private val dispatchers: AppDispatchers, ) : RemoteMediator() { - @VisibleForTesting - var lastRequestEnd: Int = 0 + @VisibleForTesting var lastRequestEnd: Int = 0 override suspend fun load( - loadType: LoadType, state: PagingState + loadType: LoadType, + state: PagingState ): MediatorResult { - logger.v { "load() called with: lastRequestEnd = $lastRequestEnd, loadType = $loadType, state = $state" } + logger.v { + "load() called with: lastRequestEnd = $lastRequestEnd, loadType = $loadType, state = $state" + } if (loadType == PREPEND) { logger.i { "load: early exit, PREPEND isn't supported" } @@ -44,17 +48,17 @@ class RecipesRemoteMediator @Inject constructor( val start = if (loadType == REFRESH) 0 else lastRequestEnd val limit = if (loadType == REFRESH) state.config.initialLoadSize else state.config.pageSize - val count: Int = runCatchingExceptCancel { - updateRecipes(start, limit, loadType) - }.getOrElse { - logger.e(it) { "load: can't load recipes" } - return MediatorResult.Error(it) - } + val count: Int = + runCatchingExceptCancel { updateRecipes(start, limit, loadType) }.getOrElse { + logger.e(it) { "load: can't load recipes" } + return MediatorResult.Error(it) + } // After something is inserted into DB the paging sources have to be invalidated // But for some reason Room/Paging library don't do it automatically // Here we invalidate them manually. - // Read that trick here https://github.com/android/architecture-components-samples/issues/889#issuecomment-880847858 + // Read that trick here + // https://github.com/android/architecture-components-samples/issues/889#issuecomment-880847858 pagingSourceFactory.invalidate() logger.d { "load: expectedCount = $limit, received $count" } @@ -63,25 +67,30 @@ class RecipesRemoteMediator @Inject constructor( } suspend fun updateRecipes( - start: Int, - limit: Int, - loadType: LoadType = REFRESH, + start: Int, + limit: Int, + loadType: LoadType = REFRESH, ): Int = coroutineScope { - logger.v { "updateRecipes() called with: start = $start, limit = $limit, loadType = $loadType" } - val deferredRecipes = async { network.requestRecipes(start, limit) } - val favorites = runCatchingExceptCancel { - network.getFavoriteRecipes() - }.getOrDefault(emptyList()).toHashSet() - val recipes = deferredRecipes.await() - val entities = withContext(dispatchers.default) { - recipes.map { recipe -> - val isFavorite = favorites.contains(recipe.slug) - modelMapper.toRecipeSummaryEntity(recipe, isFavorite) - } + logger.v { + "updateRecipes() called with: start = $start, limit = $limit, loadType = $loadType" } - if (loadType == REFRESH) storage.refreshAll(entities) - else storage.saveRecipes(entities) + val deferredRecipes = async { network.requestRecipes(start, limit) } + val favorites = + runCatchingExceptCancel { network.getFavoriteRecipes() } + .getOrDefault(emptyList()) + .toHashSet() + + val recipes = deferredRecipes.await() + + val entities = + withContext(dispatchers.default) { + recipes.map { recipe -> + val isFavorite = favorites.contains(recipe.slug) + modelMapper.toRecipeSummaryEntity(recipe, isFavorite) + } + } + + if (loadType == REFRESH) storage.refreshAll(entities) else storage.saveRecipes(entities) recipes.size } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/atridad/mealient/ui/recipes/list/RecipesListViewModel.kt b/app/src/main/java/com/atridad/mealient/ui/recipes/list/RecipesListViewModel.kt index 40a7b00..761896c 100644 --- a/app/src/main/java/com/atridad/mealient/ui/recipes/list/RecipesListViewModel.kt +++ b/app/src/main/java/com/atridad/mealient/ui/recipes/list/RecipesListViewModel.kt @@ -5,13 +5,14 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map -import dagger.hilt.android.lifecycle.HiltViewModel import com.atridad.mealient.architecture.valueUpdatesOnly import com.atridad.mealient.data.auth.AuthRepo import com.atridad.mealient.data.recipes.RecipeRepo import com.atridad.mealient.data.recipes.impl.RecipeImageUrlProvider import com.atridad.mealient.database.recipe.entity.RecipeSummaryEntity import com.atridad.mealient.logging.Logger +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -23,44 +24,48 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel -internal class RecipesListViewModel @Inject constructor( - private val recipeRepo: RecipeRepo, - private val logger: Logger, - private val recipeImageUrlProvider: RecipeImageUrlProvider, - authRepo: AuthRepo, +internal class RecipesListViewModel +@Inject +constructor( + private val recipeRepo: RecipeRepo, + private val logger: Logger, + private val recipeImageUrlProvider: RecipeImageUrlProvider, + authRepo: AuthRepo, ) : ViewModel() { private val pagingData: Flow> = - recipeRepo.createPager().flow.cachedIn(viewModelScope) + recipeRepo.createPager().flow.cachedIn(viewModelScope) private val showFavoriteIcon: StateFlow = - authRepo.isAuthorizedFlow.stateIn(viewModelScope, SharingStarted.Eagerly, false) + authRepo.isAuthorizedFlow.stateIn(viewModelScope, SharingStarted.Eagerly, false) private val pagingDataRecipeState: Flow> = - pagingData.combine(showFavoriteIcon) { data, showFavorite -> - data.map { item -> - val imageUrl = recipeImageUrlProvider.generateImageUrl(item.imageId) - RecipeListItemState( - imageUrl = imageUrl, - showFavoriteIcon = showFavorite, - entity = item, - ) + pagingData.combine(showFavoriteIcon) { data, showFavorite -> + data.map { item -> + val imageUrl = recipeImageUrlProvider.generateImageUrl(item.imageId) + RecipeListItemState( + imageUrl = imageUrl, + showFavoriteIcon = showFavorite, + entity = item, + ) + } } - } - private val _screenState = MutableStateFlow( - RecipeListState(pagingDataRecipeState = pagingDataRecipeState) - ) - val screenState: StateFlow get() = _screenState.asStateFlow() + private val _screenState = + MutableStateFlow(RecipeListState(pagingDataRecipeState = pagingDataRecipeState)) + val screenState: StateFlow + get() = _screenState.asStateFlow() init { - authRepo.isAuthorizedFlow.valueUpdatesOnly().onEach { hasAuthorized -> - logger.v { "Authorization state changed to $hasAuthorized" } - if (hasAuthorized) recipeRepo.refreshRecipes() - }.launchIn(viewModelScope) + authRepo.isAuthorizedFlow + .valueUpdatesOnly() + .onEach { hasAuthorized -> + logger.v { "Authorization state changed to $hasAuthorized" } + if (hasAuthorized) recipeRepo.refreshRecipes() + } + .launchIn(viewModelScope) } private fun onRecipeClicked(entity: RecipeSummaryEntity) { @@ -75,23 +80,23 @@ internal class RecipesListViewModel @Inject constructor( private fun onFavoriteIconClick(recipeSummaryEntity: RecipeSummaryEntity) { logger.v { "onFavoriteIconClick() called with: recipeSummaryEntity = $recipeSummaryEntity" } viewModelScope.launch { - val result = recipeRepo.updateIsRecipeFavorite( - recipeSlug = recipeSummaryEntity.slug, - isFavorite = recipeSummaryEntity.isFavorite.not(), - ) - val snackbar = result.fold( - onSuccess = { isFavorite -> - val name = recipeSummaryEntity.name - if (isFavorite) { - RecipeListSnackbar.FavoriteAdded(name) - } else { - RecipeListSnackbar.FavoriteRemoved(name) - } - }, - onFailure = { - RecipeListSnackbar.FavoriteUpdateFailed - } - ) + val result = + recipeRepo.updateIsRecipeFavorite( + recipeSlug = recipeSummaryEntity.slug, + isFavorite = recipeSummaryEntity.isFavorite.not(), + ) + val snackbar = + result.fold( + onSuccess = { _ -> + val name = recipeSummaryEntity.name + if (recipeSummaryEntity.isFavorite) { + RecipeListSnackbar.FavoriteRemoved(name) + } else { + RecipeListSnackbar.FavoriteAdded(name) + } + }, + onFailure = { RecipeListSnackbar.FavoriteUpdateFailed } + ) _screenState.update { it.copy(snackbarState = snackbar) } } } @@ -101,10 +106,11 @@ internal class RecipesListViewModel @Inject constructor( viewModelScope.launch { val result = recipeRepo.deleteRecipe(recipeSummaryEntity) logger.d { "onDeleteConfirm: delete result is $result" } - val snackbar = result.fold( - onSuccess = { null }, - onFailure = { RecipeListSnackbar.DeleteFailed }, - ) + val snackbar = + result.fold( + onSuccess = { null }, + onFailure = { RecipeListSnackbar.DeleteFailed }, + ) _screenState.update { it.copy(snackbarState = snackbar) } } } @@ -138,10 +144,10 @@ internal class RecipesListViewModel @Inject constructor( } internal data class RecipeListState( - val pagingDataRecipeState: Flow>, - val snackbarState: RecipeListSnackbar? = null, - val recipeIdToOpen: String? = null, - val searchQuery: String = "", + val pagingDataRecipeState: Flow>, + val snackbarState: RecipeListSnackbar? = null, + val recipeIdToOpen: String? = null, + val searchQuery: String = "", ) internal sealed interface RecipeListEvent { @@ -157,4 +163,4 @@ internal sealed interface RecipeListEvent { data object SnackbarShown : RecipeListEvent data class SearchQueryChanged(val query: String) : RecipeListEvent -} \ No newline at end of file +} diff --git a/app/src/main/java/com/atridad/mealient/ui/recipes/list/SearchTextField.kt b/app/src/main/java/com/atridad/mealient/ui/recipes/list/SearchTextField.kt index c84ee51..fd35737 100644 --- a/app/src/main/java/com/atridad/mealient/ui/recipes/list/SearchTextField.kt +++ b/app/src/main/java/com/atridad/mealient/ui/recipes/list/SearchTextField.kt @@ -54,7 +54,11 @@ internal fun SearchTextField( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent + errorIndicatorColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + errorContainerColor = Color.Transparent ) ) } @@ -69,4 +73,4 @@ private fun SearchTextFieldPreview() { placeholder = R.string.search_recipes_hint, ) } -} \ No newline at end of file +} diff --git a/database/src/main/kotlin/com/atridad/mealient/database/recipe/RecipeDao.kt b/database/src/main/kotlin/com/atridad/mealient/database/recipe/RecipeDao.kt index 8071411..1e4378d 100644 --- a/database/src/main/kotlin/com/atridad/mealient/database/recipe/RecipeDao.kt +++ b/database/src/main/kotlin/com/atridad/mealient/database/recipe/RecipeDao.kt @@ -9,22 +9,21 @@ 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 recipe_summaries_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 insertRecipeSummaries(recipeSummaryEntity: Iterable) - @Transaction - @Query("DELETE FROM recipe_summaries") - suspend fun removeAllRecipes() + @Transaction @Query("DELETE FROM recipe_summaries") suspend fun removeAllRecipes() @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 insertRecipe(recipe: RecipeEntity) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertRecipes(recipe: List) @@ -36,19 +35,25 @@ internal interface RecipeDao { suspend fun insertRecipeIngredients(ingredients: List) @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertIngredientToInstructionEntities(entities: List) + suspend fun insertIngredientToInstructionEntities( + entities: List + ) @Transaction - @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) // The lint is wrong, the columns are actually used + @SuppressWarnings( + RoomWarnings.CURSOR_MISMATCH + ) // The lint is wrong, the columns are actually used @Query( - "SELECT * FROM recipe " + - "JOIN recipe_summaries USING(recipe_id) " + - "LEFT JOIN recipe_ingredient USING(recipe_id) " + - "LEFT JOIN recipe_instruction USING(recipe_id) " + - "LEFT JOIN recipe_ingredient_to_instruction USING(recipe_id) " + - "WHERE recipe.recipe_id = :recipeId" + "SELECT * FROM recipe " + + "JOIN recipe_summaries USING(recipe_id) " + + "LEFT JOIN recipe_ingredient USING(recipe_id) " + + "LEFT JOIN recipe_instruction USING(recipe_id) " + + "LEFT JOIN recipe_ingredient_to_instruction USING(recipe_id) " + + "WHERE recipe.recipe_id = :recipeId" ) - suspend fun queryFullRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions? + suspend fun queryFullRecipeInfo( + recipeId: String + ): RecipeWithSummaryAndIngredientsAndInstructions? @Query("DELETE FROM recipe_ingredient WHERE recipe_id IN (:recipeIds)") suspend fun deleteRecipeIngredients(vararg recipeIds: String) @@ -59,12 +64,18 @@ internal interface RecipeDao { @Query("DELETE FROM recipe_ingredient_to_instruction WHERE recipe_id IN (:recipeIds)") suspend fun deleteRecipeIngredientToInstructions(vararg recipeIds: String) - @Query("UPDATE recipe_summaries SET recipe_summaries_is_favorite = 1 WHERE recipe_summaries_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 recipe_summaries_is_favorite = 0 WHERE recipe_summaries_slug NOT IN (:favorites)") + @Query("UPDATE recipe_summaries SET recipe_summaries_is_favorite = 0") + suspend fun setAllNonFavorite() + + @Query( + "UPDATE recipe_summaries SET recipe_summaries_is_favorite = 0 WHERE recipe_summaries_slug NOT IN (:favorites)" + ) suspend fun setNonFavorite(favorites: List) - @Delete - suspend fun deleteRecipe(entity: RecipeSummaryEntity) -} \ No newline at end of file + @Delete suspend fun deleteRecipe(entity: RecipeSummaryEntity) +} diff --git a/database/src/main/kotlin/com/atridad/mealient/database/recipe/RecipeStorageImpl.kt b/database/src/main/kotlin/com/atridad/mealient/database/recipe/RecipeStorageImpl.kt index 76aefd9..af2cedd 100644 --- a/database/src/main/kotlin/com/atridad/mealient/database/recipe/RecipeStorageImpl.kt +++ b/database/src/main/kotlin/com/atridad/mealient/database/recipe/RecipeStorageImpl.kt @@ -12,10 +12,12 @@ import com.atridad.mealient.database.recipe.entity.RecipeWithSummaryAndIngredien import com.atridad.mealient.logging.Logger import javax.inject.Inject -internal class RecipeStorageImpl @Inject constructor( - private val db: AppDb, - private val logger: Logger, - private val recipeDao: RecipeDao, +internal class RecipeStorageImpl +@Inject +constructor( + private val db: AppDb, + private val logger: Logger, + private val recipeDao: RecipeDao, ) : RecipeStorage { override suspend fun saveRecipes(recipes: List) { @@ -43,12 +45,14 @@ internal class RecipeStorageImpl @Inject constructor( } override suspend fun saveRecipeInfo( - recipe: RecipeEntity, - ingredients: List, - instructions: List, - ingredientToInstruction: List, + recipe: RecipeEntity, + ingredients: List, + instructions: List, + ingredientToInstruction: List, ) { - logger.v { "saveRecipeInfo() called with: recipe = $recipe, ingredients = $ingredients, instructions = $instructions, ingredientToInstructions = $ingredientToInstruction" } + logger.v { + "saveRecipeInfo() called with: recipe = $recipe, ingredients = $ingredients, instructions = $instructions, ingredientToInstructions = $ingredientToInstruction" + } db.withTransaction { recipeDao.insertRecipe(recipe) @@ -63,7 +67,9 @@ internal class RecipeStorageImpl @Inject constructor( } } - override suspend fun queryRecipeInfo(recipeId: String): RecipeWithSummaryAndIngredientsAndInstructions? { + override suspend fun queryRecipeInfo( + recipeId: String + ): RecipeWithSummaryAndIngredientsAndInstructions? { logger.v { "queryRecipeInfo() called with: recipeId = $recipeId" } val fullRecipeInfo = recipeDao.queryFullRecipeInfo(recipeId) logger.v { "queryRecipeInfo() returned: $fullRecipeInfo" } @@ -73,8 +79,12 @@ internal class RecipeStorageImpl @Inject constructor( override suspend fun updateFavoriteRecipes(favorites: List) { logger.v { "updateFavoriteRecipes() called with: favorites = $favorites" } db.withTransaction { - recipeDao.setFavorite(favorites) - recipeDao.setNonFavorite(favorites) + if (favorites.isNotEmpty()) { + recipeDao.setFavorite(favorites) + recipeDao.setNonFavorite(favorites) + } else { + recipeDao.setAllNonFavorite() + } } } @@ -82,4 +92,4 @@ internal class RecipeStorageImpl @Inject constructor( logger.v { "deleteRecipeBySlug() called with: entity = $entity" } recipeDao.deleteRecipe(entity) } -} \ No newline at end of file +} diff --git a/datasource/src/main/kotlin/com/atridad/mealient/datasource/MealieDataSource.kt b/datasource/src/main/kotlin/com/atridad/mealient/datasource/MealieDataSource.kt index 4982bdc..53fab2f 100644 --- a/datasource/src/main/kotlin/com/atridad/mealient/datasource/MealieDataSource.kt +++ b/datasource/src/main/kotlin/com/atridad/mealient/datasource/MealieDataSource.kt @@ -12,6 +12,7 @@ import com.atridad.mealient.datasource.models.GetShoppingListItemResponse import com.atridad.mealient.datasource.models.GetShoppingListResponse import com.atridad.mealient.datasource.models.GetShoppingListsResponse import com.atridad.mealient.datasource.models.GetUnitsResponse +import com.atridad.mealient.datasource.models.GetUserFavoritesResponse import com.atridad.mealient.datasource.models.GetUserInfoResponse import com.atridad.mealient.datasource.models.ParseRecipeURLRequest import com.atridad.mealient.datasource.models.UpdateRecipeRequest @@ -20,39 +21,37 @@ import com.atridad.mealient.datasource.models.VersionResponse interface MealieDataSource { suspend fun createRecipe( - recipe: CreateRecipeRequest, + recipe: CreateRecipeRequest, ): String suspend fun updateRecipe( - slug: String, - recipe: UpdateRecipeRequest, + slug: String, + recipe: UpdateRecipeRequest, ): GetRecipeResponse - /** - * Tries to acquire authentication token using the provided credentials - */ + /** Tries to acquire authentication token using the provided credentials */ suspend fun authenticate( - username: String, - password: String, + username: String, + password: String, ): String suspend fun getVersionInfo(baseURL: String): VersionResponse suspend fun requestRecipes( - page: Int, - perPage: Int, + page: Int, + perPage: Int, ): List suspend fun requestRecipeInfo( - slug: String, + slug: String, ): GetRecipeResponse suspend fun parseRecipeFromURL( - request: ParseRecipeURLRequest, + request: ParseRecipeURLRequest, ): String suspend fun createApiToken( - request: CreateApiTokenRequest, + request: CreateApiTokenRequest, ): CreateApiTokenResponse suspend fun requestUserInfo(): GetUserInfoResponse @@ -82,4 +81,6 @@ interface MealieDataSource { suspend fun deleteShoppingList(id: String) suspend fun updateShoppingListName(id: String, name: String) -} \ No newline at end of file + + suspend fun getUserFavoritesAlternative(userId: String): GetUserFavoritesResponse +} diff --git a/datasource/src/main/kotlin/com/atridad/mealient/datasource/MealieService.kt b/datasource/src/main/kotlin/com/atridad/mealient/datasource/MealieService.kt index bf36ec8..7fcc6b1 100644 --- a/datasource/src/main/kotlin/com/atridad/mealient/datasource/MealieService.kt +++ b/datasource/src/main/kotlin/com/atridad/mealient/datasource/MealieService.kt @@ -12,6 +12,7 @@ import com.atridad.mealient.datasource.models.GetShoppingListResponse import com.atridad.mealient.datasource.models.GetShoppingListsResponse import com.atridad.mealient.datasource.models.GetTokenResponse import com.atridad.mealient.datasource.models.GetUnitsResponse +import com.atridad.mealient.datasource.models.GetUserFavoritesResponse import com.atridad.mealient.datasource.models.GetUserInfoResponse import com.atridad.mealient.datasource.models.ParseRecipeURLRequest import com.atridad.mealient.datasource.models.UpdateRecipeRequest @@ -25,8 +26,8 @@ internal interface MealieService { suspend fun createRecipe(addRecipeRequest: CreateRecipeRequest): String suspend fun updateRecipe( - addRecipeRequest: UpdateRecipeRequest, - slug: String, + addRecipeRequest: UpdateRecipeRequest, + slug: String, ): GetRecipeResponse suspend fun getVersion(baseURL: String): VersionResponse @@ -68,6 +69,8 @@ internal interface MealieService { suspend fun deleteShoppingList(id: String) suspend fun updateShoppingList(id: String, request: JsonElement) - - suspend fun getShoppingListJson(id: String) : JsonElement -} \ No newline at end of file + + suspend fun getShoppingListJson(id: String): JsonElement + + suspend fun getUserFavoritesAlternative(userId: String): GetUserFavoritesResponse +} diff --git a/datasource/src/main/kotlin/com/atridad/mealient/datasource/impl/MealieDataSourceImpl.kt b/datasource/src/main/kotlin/com/atridad/mealient/datasource/impl/MealieDataSourceImpl.kt index 17f36aa..74eb66c 100644 --- a/datasource/src/main/kotlin/com/atridad/mealient/datasource/impl/MealieDataSourceImpl.kt +++ b/datasource/src/main/kotlin/com/atridad/mealient/datasource/impl/MealieDataSourceImpl.kt @@ -17,6 +17,7 @@ import com.atridad.mealient.datasource.models.GetShoppingListItemResponse import com.atridad.mealient.datasource.models.GetShoppingListResponse import com.atridad.mealient.datasource.models.GetShoppingListsResponse import com.atridad.mealient.datasource.models.GetUnitsResponse +import com.atridad.mealient.datasource.models.GetUserFavoritesResponse import com.atridad.mealient.datasource.models.GetUserInfoResponse import com.atridad.mealient.datasource.models.ParseRecipeURLRequest import com.atridad.mealient.datasource.models.UpdateRecipeRequest @@ -24,251 +25,309 @@ import com.atridad.mealient.datasource.models.VersionResponse import io.ktor.client.call.NoTransformationFoundException import io.ktor.client.call.body import io.ktor.client.plugins.ResponseException +import java.net.SocketException +import java.net.SocketTimeoutException +import javax.inject.Inject import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonObject -import java.net.SocketException -import java.net.SocketTimeoutException -import javax.inject.Inject -internal class MealieDataSourceImpl @Inject constructor( - private val networkRequestWrapper: NetworkRequestWrapper, - private val service: MealieService, +internal class MealieDataSourceImpl +@Inject +constructor( + private val networkRequestWrapper: NetworkRequestWrapper, + private val service: MealieService, ) : MealieDataSource { override suspend fun createRecipe( - recipe: CreateRecipeRequest, - ): String = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.createRecipe(recipe) }, - logMethod = { "createRecipe" }, - logParameters = { "recipe = $recipe" } - ).trim('"') + recipe: CreateRecipeRequest, + ): String = + networkRequestWrapper + .makeCallAndHandleUnauthorized( + block = { service.createRecipe(recipe) }, + logMethod = { "createRecipe" }, + logParameters = { "recipe = $recipe" } + ) + .trim('"') override suspend fun updateRecipe( - slug: String, - recipe: UpdateRecipeRequest, - ): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.updateRecipe(recipe, slug) }, - logMethod = { "updateRecipe" }, - logParameters = { "slug = $slug, recipe = $recipe" } - ) + slug: String, + recipe: UpdateRecipeRequest, + ): GetRecipeResponse = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.updateRecipe(recipe, slug) }, + logMethod = { "updateRecipe" }, + logParameters = { "slug = $slug, recipe = $recipe" } + ) override suspend fun authenticate( - username: String, - password: String, - ): String = networkRequestWrapper.makeCall( - block = { service.getToken(username, password) }, - logMethod = { "authenticate" }, - logParameters = { "username = $username, password = $password" } - ).map { it.accessToken }.getOrElse { - val errorDetail = (it as? ResponseException)?.response?.body() ?: throw it - throw if (errorDetail.detail == "Unauthorized") NetworkError.Unauthorized(it) else it - } + username: String, + password: String, + ): String = + networkRequestWrapper + .makeCall( + block = { service.getToken(username, password) }, + logMethod = { "authenticate" }, + logParameters = { "username = $username, password = $password" } + ) + .map { it.accessToken } + .getOrElse { + val errorDetail = + (it as? ResponseException)?.response?.body() + ?: throw it + throw if (errorDetail.detail == "Unauthorized") + NetworkError.Unauthorized(it) + else it + } override suspend fun getVersionInfo(baseURL: String): VersionResponse = - networkRequestWrapper.makeCall( - block = { service.getVersion(baseURL) }, - logMethod = { "getVersionInfo" }, - logParameters = { "baseURL = $baseURL" } - ).getOrElse { - throw when (it) { - is ResponseException, is NoTransformationFoundException -> NetworkError.NotMealie(it) - is SocketTimeoutException, is SocketException -> NetworkError.NoServerConnection(it) - else -> NetworkError.MalformedUrl(it) - } - } + networkRequestWrapper.makeCall( + block = { service.getVersion(baseURL) }, + logMethod = { "getVersionInfo" }, + logParameters = { "baseURL = $baseURL" } + ) + .getOrElse { + throw when (it) { + is ResponseException, is NoTransformationFoundException -> + NetworkError.NotMealie(it) + is SocketTimeoutException, is SocketException -> + NetworkError.NoServerConnection(it) + else -> NetworkError.MalformedUrl(it) + } + } override suspend fun requestRecipes( - page: Int, - perPage: Int, - ): List = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getRecipeSummary(page, perPage) }, - logMethod = { "requestRecipes" }, - logParameters = { "page = $page, perPage = $perPage" } - ).items + page: Int, + perPage: Int, + ): List { + val response = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getRecipeSummary(page, perPage) }, + logMethod = { "requestRecipes" }, + logParameters = { "page = $page, perPage = $perPage" } + ) + + return response.items + } override suspend fun requestRecipeInfo( - slug: String, - ): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getRecipe(slug) }, - logMethod = { "requestRecipeInfo" }, - logParameters = { "slug = $slug" } - ) + slug: String, + ): GetRecipeResponse = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getRecipe(slug) }, + logMethod = { "requestRecipeInfo" }, + logParameters = { "slug = $slug" } + ) override suspend fun parseRecipeFromURL( - request: ParseRecipeURLRequest, - ): String = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.createRecipeFromURL(request) }, - logMethod = { "parseRecipeFromURL" }, - logParameters = { "request = $request" } - ) + request: ParseRecipeURLRequest, + ): String = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.createRecipeFromURL(request) }, + logMethod = { "parseRecipeFromURL" }, + logParameters = { "request = $request" } + ) override suspend fun createApiToken( - request: CreateApiTokenRequest, - ): CreateApiTokenResponse = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.createApiToken(request) }, - logMethod = { "createApiToken" }, - logParameters = { "request = $request" } - ) + request: CreateApiTokenRequest, + ): CreateApiTokenResponse = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.createApiToken(request) }, + logMethod = { "createApiToken" }, + logParameters = { "request = $request" } + ) override suspend fun requestUserInfo(): GetUserInfoResponse { - return networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getUserSelfInfo() }, - logMethod = { "requestUserInfo" }, - ) + val response = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getUserSelfInfo() }, + logMethod = { "requestUserInfo" }, + ) + + return response } override suspend fun removeFavoriteRecipe( - userId: String, - recipeSlug: String, - ): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.removeFavoriteRecipe(userId, recipeSlug) }, - logMethod = { "removeFavoriteRecipe" }, - logParameters = { "userId = $userId, recipeSlug = $recipeSlug" } - ) + userId: String, + recipeSlug: String, + ): Unit { + + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.removeFavoriteRecipe(userId, recipeSlug) }, + logMethod = { "removeFavoriteRecipe" }, + logParameters = { "userId = $userId, recipeSlug = $recipeSlug" } + ) + } override suspend fun addFavoriteRecipe( - userId: String, - recipeSlug: String, - ): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.addFavoriteRecipe(userId, recipeSlug) }, - logMethod = { "addFavoriteRecipe" }, - logParameters = { "userId = $userId, recipeSlug = $recipeSlug" } - ) + userId: String, + recipeSlug: String, + ): Unit { + + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.addFavoriteRecipe(userId, recipeSlug) }, + logMethod = { "addFavoriteRecipe" }, + logParameters = { "userId = $userId, recipeSlug = $recipeSlug" } + ) + } override suspend fun deleteRecipe( - slug: String, - ): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.deleteRecipe(slug) }, - logMethod = { "deleteRecipe" }, - logParameters = { "slug = $slug" } - ) + slug: String, + ): Unit = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.deleteRecipe(slug) }, + logMethod = { "deleteRecipe" }, + logParameters = { "slug = $slug" } + ) override suspend fun getShoppingLists( - page: Int, - perPage: Int, - ): GetShoppingListsResponse = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getShoppingLists(page, perPage) }, - logMethod = { "getShoppingLists" }, - logParameters = { "page = $page, perPage = $perPage" } - ) + page: Int, + perPage: Int, + ): GetShoppingListsResponse = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getShoppingLists(page, perPage) }, + logMethod = { "getShoppingLists" }, + logParameters = { "page = $page, perPage = $perPage" } + ) override suspend fun getShoppingList( - id: String, - ): GetShoppingListResponse = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getShoppingList(id) }, - logMethod = { "getShoppingList" }, - logParameters = { "id = $id" } - ) + id: String, + ): GetShoppingListResponse = + 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" } - ) + 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" } - ) + id: String, + request: JsonElement, + ) = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.updateShoppingListItem(id, request) }, + logMethod = { "updateShoppingListItem" }, + logParameters = { "id = $id, request = $request" } + ) override suspend fun deleteShoppingListItem( - id: String, - ) = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.deleteShoppingListItem(id) }, - logMethod = { "deleteShoppingListItem" }, - logParameters = { "id = $id" } - ) + id: String, + ) = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.deleteShoppingListItem(id) }, + logMethod = { "deleteShoppingListItem" }, + logParameters = { "id = $id" } + ) override suspend fun updateShoppingListItem( - item: GetShoppingListItemResponse, + item: GetShoppingListItemResponse, ) { // Has to be done in two steps because we can't specify only the changed fields val remoteItem = getShoppingListItem(item.id) - val updatedItem = remoteItem.jsonObject.toMutableMap().apply { - put("checked", JsonPrimitive(item.checked)) - put("isFood", JsonPrimitive(item.isFood)) - put("note", JsonPrimitive(item.note)) - put("quantity", JsonPrimitive(item.quantity)) - put("foodId", JsonPrimitive(item.food?.id)) - put("unitId", JsonPrimitive(item.unit?.id)) - remove("unit") - remove("food") - } + val updatedItem = + remoteItem.jsonObject.toMutableMap().apply { + put("checked", JsonPrimitive(item.checked)) + put("isFood", JsonPrimitive(item.isFood)) + put("note", JsonPrimitive(item.note)) + put("quantity", JsonPrimitive(item.quantity)) + put("foodId", JsonPrimitive(item.food?.id)) + put("unitId", JsonPrimitive(item.unit?.id)) + remove("unit") + remove("food") + } updateShoppingListItem(item.id, JsonObject(updatedItem)) } override suspend fun getFoods(): GetFoodsResponse { return networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getFoods(perPage = -1) }, - logMethod = { "getFoods" }, + block = { service.getFoods(perPage = -1) }, + logMethod = { "getFoods" }, ) } override suspend fun getUnits(): GetUnitsResponse { return networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getUnits(perPage = -1) }, - logMethod = { "getUnits" }, + block = { service.getUnits(perPage = -1) }, + logMethod = { "getUnits" }, ) } override suspend fun addShoppingListItem( - request: CreateShoppingListItemRequest, - ) = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.createShoppingListItem(request) }, - logMethod = { "addShoppingListItem" }, - logParameters = { "request = $request" } - ) + request: CreateShoppingListItemRequest, + ) = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.createShoppingListItem(request) }, + logMethod = { "addShoppingListItem" }, + logParameters = { "request = $request" } + ) override suspend fun addShoppingList( - request: CreateShoppingListRequest, - ) = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.createShoppingList(request) }, - logMethod = { "createShoppingList" }, - logParameters = { "request = $request" } - ) + request: CreateShoppingListRequest, + ) = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.createShoppingList(request) }, + logMethod = { "createShoppingList" }, + logParameters = { "request = $request" } + ) private suspend fun updateShoppingList( - id: String, - request: JsonElement, - ) = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.updateShoppingList(id, request) }, - logMethod = { "updateShoppingList" }, - logParameters = { "id = $id, request = $request" } - ) + id: String, + request: JsonElement, + ) = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.updateShoppingList(id, request) }, + logMethod = { "updateShoppingList" }, + logParameters = { "id = $id, request = $request" } + ) private suspend fun getShoppingListJson( - id: String, - ) = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.getShoppingListJson(id) }, - logMethod = { "getShoppingListJson" }, - logParameters = { "id = $id" } - ) + id: String, + ) = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getShoppingListJson(id) }, + logMethod = { "getShoppingListJson" }, + logParameters = { "id = $id" } + ) override suspend fun deleteShoppingList( - id: String, - ) = networkRequestWrapper.makeCallAndHandleUnauthorized( - block = { service.deleteShoppingList(id) }, - logMethod = { "deleteShoppingList" }, - logParameters = { "id = $id" } - ) + id: String, + ) = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.deleteShoppingList(id) }, + logMethod = { "deleteShoppingList" }, + logParameters = { "id = $id" } + ) - override suspend fun updateShoppingListName( - id: String, - name: String - ) { + override suspend fun updateShoppingListName(id: String, name: String) { // Has to be done in two steps because we can't specify only the changed fields val remoteItem = getShoppingListJson(id) - val updatedItem = remoteItem.jsonObject.toMutableMap().apply { - put("name", JsonPrimitive(name)) - }.let(::JsonObject) + val updatedItem = + remoteItem + .jsonObject + .toMutableMap() + .apply { put("name", JsonPrimitive(name)) } + .let(::JsonObject) updateShoppingList(id, updatedItem) } + + override suspend fun getUserFavoritesAlternative(userId: String): GetUserFavoritesResponse { + + val response = + networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getUserFavoritesAlternative(userId) }, + logMethod = { "getUserFavoritesAlternative" }, + logParameters = { "userId = $userId" } + ) + + return response + } } diff --git a/datasource/src/main/kotlin/com/atridad/mealient/datasource/impl/MealieServiceKtor.kt b/datasource/src/main/kotlin/com/atridad/mealient/datasource/impl/MealieServiceKtor.kt index 3313604..7e27c25 100644 --- a/datasource/src/main/kotlin/com/atridad/mealient/datasource/impl/MealieServiceKtor.kt +++ b/datasource/src/main/kotlin/com/atridad/mealient/datasource/impl/MealieServiceKtor.kt @@ -14,6 +14,7 @@ import com.atridad.mealient.datasource.models.GetShoppingListResponse import com.atridad.mealient.datasource.models.GetShoppingListsResponse import com.atridad.mealient.datasource.models.GetTokenResponse import com.atridad.mealient.datasource.models.GetUnitsResponse +import com.atridad.mealient.datasource.models.GetUserFavoritesResponse import com.atridad.mealient.datasource.models.GetUserInfoResponse import com.atridad.mealient.datasource.models.ParseRecipeURLRequest import com.atridad.mealient.datasource.models.UpdateRecipeRequest @@ -34,13 +35,15 @@ import io.ktor.http.contentType import io.ktor.http.parameters import io.ktor.http.path import io.ktor.http.takeFrom -import kotlinx.serialization.json.JsonElement import javax.inject.Inject import javax.inject.Provider +import kotlinx.serialization.json.JsonElement -internal class MealieServiceKtor @Inject constructor( - private val httpClient: HttpClient, - private val serverUrlProviderProvider: Provider, +internal class MealieServiceKtor +@Inject +constructor( + private val httpClient: HttpClient, + private val serverUrlProviderProvider: Provider, ) : MealieService { private val serverUrlProvider: ServerUrlProvider @@ -52,111 +55,109 @@ internal class MealieServiceKtor @Inject constructor( append("password", password) } - return httpClient.post { - endpoint("/api/auth/token") - setBody(FormDataContent(formParameters)) - }.body() + return httpClient + .post { + endpoint("/api/auth/token") + setBody(FormDataContent(formParameters)) + } + .body() } override suspend fun createRecipe(addRecipeRequest: CreateRecipeRequest): String { - return httpClient.post { - endpoint("/api/recipes") - contentType(ContentType.Application.Json) - setBody(addRecipeRequest) - }.body() + return httpClient + .post { + endpoint("/api/recipes") + contentType(ContentType.Application.Json) + setBody(addRecipeRequest) + } + .body() } override suspend fun updateRecipe( - addRecipeRequest: UpdateRecipeRequest, - slug: String, + addRecipeRequest: UpdateRecipeRequest, + slug: String, ): GetRecipeResponse { - return httpClient.patch { - endpoint("/api/recipes/$slug") - contentType(ContentType.Application.Json) - setBody(addRecipeRequest) - }.body() + return httpClient + .patch { + endpoint("/api/recipes/$slug") + contentType(ContentType.Application.Json) + setBody(addRecipeRequest) + } + .body() } override suspend fun getVersion(baseURL: String): VersionResponse { - return httpClient.get { - endpoint(baseURL, "/api/app/about") - }.body() + return httpClient.get { endpoint(baseURL, "/api/app/about") }.body() } override suspend fun getRecipeSummary(page: Int, perPage: Int): GetRecipesResponse { - return httpClient.get { - endpoint("/api/recipes") { - parameters.append("page", page.toString()) - parameters.append("perPage", perPage.toString()) - } - }.body() + return httpClient + .get { + endpoint("/api/recipes") { + parameters.append("page", page.toString()) + parameters.append("perPage", perPage.toString()) + } + } + .body() } override suspend fun getRecipe(slug: String): GetRecipeResponse { - return httpClient.get { - endpoint("/api/recipes/$slug") - }.body() + return httpClient.get { endpoint("/api/recipes/$slug") }.body() } override suspend fun createRecipeFromURL(request: ParseRecipeURLRequest): String { - return httpClient.post { - endpoint("/api/recipes/create-url") - contentType(ContentType.Application.Json) - setBody(request) - }.body() + return httpClient + .post { + endpoint("/api/recipes/create-url") + contentType(ContentType.Application.Json) + setBody(request) + } + .body() } override suspend fun createApiToken(request: CreateApiTokenRequest): CreateApiTokenResponse { - return httpClient.post { - endpoint("/api/users/api-tokens") - contentType(ContentType.Application.Json) - setBody(request) - }.body() + return httpClient + .post { + endpoint("/api/users/api-tokens") + contentType(ContentType.Application.Json) + setBody(request) + } + .body() } override suspend fun getUserSelfInfo(): GetUserInfoResponse { - return httpClient.get { - endpoint("/api/users/self") - }.body() + return httpClient.get { endpoint("/api/users/self") }.body() } override suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String) { - httpClient.delete { - endpoint("/api/users/$userId/favorites/$recipeSlug") - } + httpClient.delete { endpoint("/api/users/$userId/favorites/$recipeSlug") } } override suspend fun addFavoriteRecipe(userId: String, recipeSlug: String) { - httpClient.post { - endpoint("/api/users/$userId/favorites/$recipeSlug") - } + httpClient.post { endpoint("/api/users/$userId/favorites/$recipeSlug") } } override suspend fun deleteRecipe(slug: String) { - httpClient.delete { - endpoint("/api/recipes/$slug") - } + httpClient.delete { endpoint("/api/recipes/$slug") } } override suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponse { - return httpClient.get { - endpoint("/api/households/shopping/lists") { - parameters.append("page", page.toString()) - parameters.append("perPage", perPage.toString()) - } - }.body() + return httpClient + .get { + endpoint("/api/households/shopping/lists") { + parameters.append("page", page.toString()) + parameters.append("perPage", perPage.toString()) + } + } + .body() } override suspend fun getShoppingList(id: String): GetShoppingListResponse { - return httpClient.get { - endpoint("/api/households/shopping/lists/$id") - }.body() + return httpClient.get { endpoint("/api/households/shopping/lists/$id") }.body() } override suspend fun getShoppingListItem(id: String): JsonElement { - return httpClient.get { - endpoint("/api/households/shopping/items/$id") - }.body() + return httpClient.get { endpoint("/api/households/shopping/items/$id") }.body() } override suspend fun updateShoppingListItem(id: String, request: JsonElement) { @@ -168,25 +169,19 @@ internal class MealieServiceKtor @Inject constructor( } override suspend fun deleteShoppingListItem(id: String) { - httpClient.delete { - endpoint("/api/households/shopping/items/$id") - } + httpClient.delete { endpoint("/api/households/shopping/items/$id") } } override suspend fun getFoods(perPage: Int): GetFoodsResponse { - return httpClient.get { - endpoint("/api/foods") { - parameters.append("perPage", perPage.toString()) - } - }.body() + return httpClient + .get { endpoint("/api/foods") { parameters.append("perPage", perPage.toString()) } } + .body() } override suspend fun getUnits(perPage: Int): GetUnitsResponse { - return httpClient.get { - endpoint("/api/units") { - parameters.append("perPage", perPage.toString()) - } - }.body() + return httpClient + .get { endpoint("/api/units") { parameters.append("perPage", perPage.toString()) } } + .body() } override suspend fun createShoppingListItem(request: CreateShoppingListItemRequest) { @@ -206,9 +201,7 @@ internal class MealieServiceKtor @Inject constructor( } override suspend fun deleteShoppingList(id: String) { - httpClient.delete { - endpoint("/api/households/shopping/lists/$id") - } + httpClient.delete { endpoint("/api/households/shopping/lists/$id") } } override suspend fun updateShoppingList(id: String, request: JsonElement) { @@ -220,27 +213,25 @@ internal class MealieServiceKtor @Inject constructor( } override suspend fun getShoppingListJson(id: String): JsonElement { - return httpClient.get { - endpoint("/api/households/shopping/lists/$id") - }.body() + return httpClient.get { endpoint("/api/households/shopping/lists/$id") }.body() + } + + override suspend fun getUserFavoritesAlternative(userId: String): GetUserFavoritesResponse { + return httpClient.get { endpoint("/api/users/$userId/favorites") }.body() } private suspend fun HttpRequestBuilder.endpoint( - path: String, - block: URLBuilder.() -> Unit = {}, + path: String, + block: URLBuilder.() -> Unit = {}, ) { val baseUrl = checkNotNull(serverUrlProvider.getUrl()) { "Server URL is not set" } - endpoint( - baseUrl = baseUrl, - path = path, - block = block - ) + endpoint(baseUrl = baseUrl, path = path, block = block) } private fun HttpRequestBuilder.endpoint( - baseUrl: String, - path: String, - block: URLBuilder.() -> Unit = {}, + baseUrl: String, + path: String, + block: URLBuilder.() -> Unit = {}, ) { url { takeFrom(baseUrl) diff --git a/datasource/src/main/kotlin/com/atridad/mealient/datasource/models/GetUserFavoritesResponse.kt b/datasource/src/main/kotlin/com/atridad/mealient/datasource/models/GetUserFavoritesResponse.kt new file mode 100644 index 0000000..3a24544 --- /dev/null +++ b/datasource/src/main/kotlin/com/atridad/mealient/datasource/models/GetUserFavoritesResponse.kt @@ -0,0 +1,18 @@ +package com.atridad.mealient.datasource.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetUserFavoritesResponse( + @SerialName("ratings") val ratings: List = emptyList(), +) + +@Serializable +data class UserRatingResponse( + @SerialName("recipeId") val recipeId: String, + @SerialName("rating") val rating: Double? = null, + @SerialName("isFavorite") val isFavorite: Boolean, + @SerialName("userId") val userId: String, + @SerialName("id") val id: String, +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 85b6800..b272b28 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] # https://maven.google.com/web/index.html?q=com.android.tools.build#com.android.tools.build:gradle -androidGradlePlugin = "8.5.2" +androidGradlePlugin = "8.9.0" # https://github.com/JetBrains/kotlin/releases kotlin = "2.0.10" # https://dagger.dev/hilt/gradle-setup diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fb602ee..07faef0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME