Fixed a bug with favourites
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/release/com.atridad.mealient_0.5.1.apk
Normal file
BIN
app/release/com.atridad.mealient_0.5.1.apk
Normal file
Binary file not shown.
@@ -11,8 +11,8 @@
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 37,
|
||||
"versionName": "0.5.0",
|
||||
"versionCode": 38,
|
||||
"versionName": "0.5.1",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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<GetRecipeSummaryResponse> {
|
||||
// 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<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Int, RecipeSummaryEntity>() {
|
||||
|
||||
@VisibleForTesting
|
||||
var lastRequestEnd: Int = 0
|
||||
@VisibleForTesting var lastRequestEnd: Int = 0
|
||||
|
||||
override suspend fun load(
|
||||
loadType: LoadType, state: PagingState<Int, RecipeSummaryEntity>
|
||||
loadType: LoadType,
|
||||
state: PagingState<Int, RecipeSummaryEntity>
|
||||
): 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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PagingData<RecipeSummaryEntity>> =
|
||||
recipeRepo.createPager().flow.cachedIn(viewModelScope)
|
||||
recipeRepo.createPager().flow.cachedIn(viewModelScope)
|
||||
|
||||
private val showFavoriteIcon: StateFlow<Boolean> =
|
||||
authRepo.isAuthorizedFlow.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||
authRepo.isAuthorizedFlow.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||
|
||||
private val pagingDataRecipeState: Flow<PagingData<RecipeListItemState>> =
|
||||
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<RecipeListState> get() = _screenState.asStateFlow()
|
||||
private val _screenState =
|
||||
MutableStateFlow(RecipeListState(pagingDataRecipeState = pagingDataRecipeState))
|
||||
val screenState: StateFlow<RecipeListState>
|
||||
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<PagingData<RecipeListItemState>>,
|
||||
val snackbarState: RecipeListSnackbar? = null,
|
||||
val recipeIdToOpen: String? = null,
|
||||
val searchQuery: String = "",
|
||||
val pagingDataRecipeState: Flow<PagingData<RecipeListItemState>>,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,22 +9,21 @@ internal interface RecipeDao {
|
||||
@Query("SELECT * FROM recipe_summaries ORDER BY recipe_summaries_date_added DESC")
|
||||
fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity>
|
||||
|
||||
@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<Int, RecipeSummaryEntity>
|
||||
|
||||
@Transaction
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipeSummaries(recipeSummaryEntity: Iterable<RecipeSummaryEntity>)
|
||||
|
||||
@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<RecipeSummaryEntity>
|
||||
|
||||
@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<RecipeEntity>)
|
||||
@@ -36,19 +35,25 @@ internal interface RecipeDao {
|
||||
suspend fun insertRecipeIngredients(ingredients: List<RecipeIngredientEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertIngredientToInstructionEntities(entities: List<RecipeIngredientToInstructionEntity>)
|
||||
suspend fun insertIngredientToInstructionEntities(
|
||||
entities: List<RecipeIngredientToInstructionEntity>
|
||||
)
|
||||
|
||||
@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<String>)
|
||||
|
||||
@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<String>)
|
||||
|
||||
@Delete
|
||||
suspend fun deleteRecipe(entity: RecipeSummaryEntity)
|
||||
}
|
||||
@Delete suspend fun deleteRecipe(entity: RecipeSummaryEntity)
|
||||
}
|
||||
|
||||
@@ -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<RecipeSummaryEntity>) {
|
||||
@@ -43,12 +45,14 @@ internal class RecipeStorageImpl @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun saveRecipeInfo(
|
||||
recipe: RecipeEntity,
|
||||
ingredients: List<RecipeIngredientEntity>,
|
||||
instructions: List<RecipeInstructionEntity>,
|
||||
ingredientToInstruction: List<RecipeIngredientToInstructionEntity>,
|
||||
recipe: RecipeEntity,
|
||||
ingredients: List<RecipeIngredientEntity>,
|
||||
instructions: List<RecipeInstructionEntity>,
|
||||
ingredientToInstruction: List<RecipeIngredientToInstructionEntity>,
|
||||
) {
|
||||
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<String>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetRecipeSummaryResponse>
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
suspend fun getUserFavoritesAlternative(userId: String): GetUserFavoritesResponse
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
suspend fun getShoppingListJson(id: String): JsonElement
|
||||
|
||||
suspend fun getUserFavoritesAlternative(userId: String): GetUserFavoritesResponse
|
||||
}
|
||||
|
||||
@@ -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<ErrorDetail>() ?: 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<ErrorDetail>()
|
||||
?: 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<GetRecipeSummaryResponse> = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.getRecipeSummary(page, perPage) },
|
||||
logMethod = { "requestRecipes" },
|
||||
logParameters = { "page = $page, perPage = $perPage" }
|
||||
).items
|
||||
page: Int,
|
||||
perPage: Int,
|
||||
): List<GetRecipeSummaryResponse> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ServerUrlProvider>,
|
||||
internal class MealieServiceKtor
|
||||
@Inject
|
||||
constructor(
|
||||
private val httpClient: HttpClient,
|
||||
private val serverUrlProviderProvider: Provider<ServerUrlProvider>,
|
||||
) : 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)
|
||||
|
||||
@@ -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<UserRatingResponse> = 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,
|
||||
)
|
||||
@@ -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
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user