Merge pull request 'BUG FIX: Favourite state was not showing' (#2) from bugfix-round into main
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
# Mealient
|
# Mealient
|
||||||
|
|
||||||
## DISCLAIMER
|
## DISCLAIMERS
|
||||||
|
|
||||||
This project is developed independently from the core Mealie project. It is NOT associated with the
|
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
|
core Mealie developers. Any issues must be reported to the Mealient repository, NOT the Mealie
|
||||||
repository.
|
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?
|
## What is this?
|
||||||
|
|
||||||
An unofficial Android client for [Mealie](https://github.com/mealie-recipes/mealie/). It enables you
|
An unofficial Android client for [Mealie](https://github.com/mealie-recipes/mealie/). It enables you
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.atridad.mealient"
|
applicationId = "com.atridad.mealient"
|
||||||
versionCode = 37
|
versionCode = 38
|
||||||
versionName = "0.5.0"
|
versionName = "0.5.1"
|
||||||
testInstrumentationRunner = "com.atridad.mealient.MealientTestRunner"
|
testInstrumentationRunner = "com.atridad.mealient.MealientTestRunner"
|
||||||
testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true")
|
testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true")
|
||||||
resourceConfigurations += listOf("en", "es", "ru", "fr", "nl", "pt", "de")
|
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",
|
"type": "SINGLE",
|
||||||
"filters": [],
|
"filters": [],
|
||||||
"attributes": [],
|
"attributes": [],
|
||||||
"versionCode": 37,
|
"versionCode": 38,
|
||||||
"versionName": "0.5.0",
|
"versionName": "0.5.1",
|
||||||
"outputFile": "app-release.apk"
|
"outputFile": "app-release.apk"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
|
|||||||
import com.atridad.mealient.model_mapper.ModelMapper
|
import com.atridad.mealient.model_mapper.ModelMapper
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MealieDataSourceWrapper @Inject constructor(
|
class MealieDataSourceWrapper
|
||||||
private val dataSource: MealieDataSource,
|
@Inject
|
||||||
private val modelMapper: ModelMapper,
|
constructor(
|
||||||
|
private val dataSource: MealieDataSource,
|
||||||
|
private val modelMapper: ModelMapper,
|
||||||
) : AddRecipeDataSource, RecipeDataSource, ParseRecipeDataSource {
|
) : AddRecipeDataSource, RecipeDataSource, ParseRecipeDataSource {
|
||||||
|
|
||||||
override suspend fun addRecipe(recipe: AddRecipeInfo): String {
|
override suspend fun addRecipe(recipe: AddRecipeInfo): String {
|
||||||
@@ -23,10 +25,11 @@ class MealieDataSourceWrapper @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun requestRecipes(
|
override suspend fun requestRecipes(
|
||||||
start: Int,
|
start: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
): List<GetRecipeSummaryResponse> {
|
): 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
|
val page = start / limit + 1
|
||||||
return dataSource.requestRecipes(page, limit)
|
return dataSource.requestRecipes(page, limit)
|
||||||
}
|
}
|
||||||
@@ -40,11 +43,31 @@ class MealieDataSourceWrapper @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getFavoriteRecipes(): List<String> {
|
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) {
|
override suspend fun updateIsRecipeFavorite(recipeSlug: String, isFavorite: Boolean) {
|
||||||
val userId = dataSource.requestUserInfo().id
|
val userInfo = dataSource.requestUserInfo()
|
||||||
|
val userId = userInfo.id
|
||||||
|
|
||||||
if (isFavorite) {
|
if (isFavorite) {
|
||||||
dataSource.addFavoriteRecipe(userId, recipeSlug)
|
dataSource.addFavoriteRecipe(userId, recipeSlug)
|
||||||
} else {
|
} else {
|
||||||
@@ -55,4 +78,4 @@ class MealieDataSourceWrapper @Inject constructor(
|
|||||||
override suspend fun deleteRecipe(recipeSlug: String) {
|
override suspend fun deleteRecipe(recipeSlug: String) {
|
||||||
dataSource.deleteRecipe(recipeSlug)
|
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.datasource.runCatchingExceptCancel
|
||||||
import com.atridad.mealient.logging.Logger
|
import com.atridad.mealient.logging.Logger
|
||||||
import com.atridad.mealient.model_mapper.ModelMapper
|
import com.atridad.mealient.model_mapper.ModelMapper
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
@Singleton
|
@Singleton
|
||||||
class RecipesRemoteMediator @Inject constructor(
|
class RecipesRemoteMediator
|
||||||
private val storage: RecipeStorage,
|
@Inject
|
||||||
private val network: RecipeDataSource,
|
constructor(
|
||||||
private val pagingSourceFactory: RecipePagingSourceFactory,
|
private val storage: RecipeStorage,
|
||||||
private val logger: Logger,
|
private val network: RecipeDataSource,
|
||||||
private val modelMapper: ModelMapper,
|
private val pagingSourceFactory: RecipePagingSourceFactory,
|
||||||
private val dispatchers: AppDispatchers,
|
private val logger: Logger,
|
||||||
|
private val modelMapper: ModelMapper,
|
||||||
|
private val dispatchers: AppDispatchers,
|
||||||
) : RemoteMediator<Int, RecipeSummaryEntity>() {
|
) : RemoteMediator<Int, RecipeSummaryEntity>() {
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting var lastRequestEnd: Int = 0
|
||||||
var lastRequestEnd: Int = 0
|
|
||||||
|
|
||||||
override suspend fun load(
|
override suspend fun load(
|
||||||
loadType: LoadType, state: PagingState<Int, RecipeSummaryEntity>
|
loadType: LoadType,
|
||||||
|
state: PagingState<Int, RecipeSummaryEntity>
|
||||||
): MediatorResult {
|
): 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) {
|
if (loadType == PREPEND) {
|
||||||
logger.i { "load: early exit, PREPEND isn't supported" }
|
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 start = if (loadType == REFRESH) 0 else lastRequestEnd
|
||||||
val limit = if (loadType == REFRESH) state.config.initialLoadSize else state.config.pageSize
|
val limit = if (loadType == REFRESH) state.config.initialLoadSize else state.config.pageSize
|
||||||
|
|
||||||
val count: Int = runCatchingExceptCancel {
|
val count: Int =
|
||||||
updateRecipes(start, limit, loadType)
|
runCatchingExceptCancel { updateRecipes(start, limit, loadType) }.getOrElse {
|
||||||
}.getOrElse {
|
logger.e(it) { "load: can't load recipes" }
|
||||||
logger.e(it) { "load: can't load recipes" }
|
return MediatorResult.Error(it)
|
||||||
return MediatorResult.Error(it)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// After something is inserted into DB the paging sources have to be invalidated
|
// 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
|
// But for some reason Room/Paging library don't do it automatically
|
||||||
// Here we invalidate them manually.
|
// 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()
|
pagingSourceFactory.invalidate()
|
||||||
|
|
||||||
logger.d { "load: expectedCount = $limit, received $count" }
|
logger.d { "load: expectedCount = $limit, received $count" }
|
||||||
@@ -63,25 +67,30 @@ class RecipesRemoteMediator @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateRecipes(
|
suspend fun updateRecipes(
|
||||||
start: Int,
|
start: Int,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
loadType: LoadType = REFRESH,
|
loadType: LoadType = REFRESH,
|
||||||
): Int = coroutineScope {
|
): Int = coroutineScope {
|
||||||
logger.v { "updateRecipes() called with: start = $start, limit = $limit, loadType = $loadType" }
|
logger.v {
|
||||||
val deferredRecipes = async { network.requestRecipes(start, limit) }
|
"updateRecipes() called with: start = $start, limit = $limit, loadType = $loadType"
|
||||||
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)
|
val deferredRecipes = async { network.requestRecipes(start, limit) }
|
||||||
else storage.saveRecipes(entities)
|
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
|
recipes.size
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
import androidx.paging.map
|
import androidx.paging.map
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import com.atridad.mealient.architecture.valueUpdatesOnly
|
import com.atridad.mealient.architecture.valueUpdatesOnly
|
||||||
import com.atridad.mealient.data.auth.AuthRepo
|
import com.atridad.mealient.data.auth.AuthRepo
|
||||||
import com.atridad.mealient.data.recipes.RecipeRepo
|
import com.atridad.mealient.data.recipes.RecipeRepo
|
||||||
import com.atridad.mealient.data.recipes.impl.RecipeImageUrlProvider
|
import com.atridad.mealient.data.recipes.impl.RecipeImageUrlProvider
|
||||||
import com.atridad.mealient.database.recipe.entity.RecipeSummaryEntity
|
import com.atridad.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
import com.atridad.mealient.logging.Logger
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -23,44 +24,48 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class RecipesListViewModel @Inject constructor(
|
internal class RecipesListViewModel
|
||||||
private val recipeRepo: RecipeRepo,
|
@Inject
|
||||||
private val logger: Logger,
|
constructor(
|
||||||
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
private val recipeRepo: RecipeRepo,
|
||||||
authRepo: AuthRepo,
|
private val logger: Logger,
|
||||||
|
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
||||||
|
authRepo: AuthRepo,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val pagingData: Flow<PagingData<RecipeSummaryEntity>> =
|
private val pagingData: Flow<PagingData<RecipeSummaryEntity>> =
|
||||||
recipeRepo.createPager().flow.cachedIn(viewModelScope)
|
recipeRepo.createPager().flow.cachedIn(viewModelScope)
|
||||||
|
|
||||||
private val showFavoriteIcon: StateFlow<Boolean> =
|
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>> =
|
private val pagingDataRecipeState: Flow<PagingData<RecipeListItemState>> =
|
||||||
pagingData.combine(showFavoriteIcon) { data, showFavorite ->
|
pagingData.combine(showFavoriteIcon) { data, showFavorite ->
|
||||||
data.map { item ->
|
data.map { item ->
|
||||||
val imageUrl = recipeImageUrlProvider.generateImageUrl(item.imageId)
|
val imageUrl = recipeImageUrlProvider.generateImageUrl(item.imageId)
|
||||||
RecipeListItemState(
|
RecipeListItemState(
|
||||||
imageUrl = imageUrl,
|
imageUrl = imageUrl,
|
||||||
showFavoriteIcon = showFavorite,
|
showFavoriteIcon = showFavorite,
|
||||||
entity = item,
|
entity = item,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private val _screenState = MutableStateFlow(
|
private val _screenState =
|
||||||
RecipeListState(pagingDataRecipeState = pagingDataRecipeState)
|
MutableStateFlow(RecipeListState(pagingDataRecipeState = pagingDataRecipeState))
|
||||||
)
|
val screenState: StateFlow<RecipeListState>
|
||||||
val screenState: StateFlow<RecipeListState> get() = _screenState.asStateFlow()
|
get() = _screenState.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
authRepo.isAuthorizedFlow.valueUpdatesOnly().onEach { hasAuthorized ->
|
authRepo.isAuthorizedFlow
|
||||||
logger.v { "Authorization state changed to $hasAuthorized" }
|
.valueUpdatesOnly()
|
||||||
if (hasAuthorized) recipeRepo.refreshRecipes()
|
.onEach { hasAuthorized ->
|
||||||
}.launchIn(viewModelScope)
|
logger.v { "Authorization state changed to $hasAuthorized" }
|
||||||
|
if (hasAuthorized) recipeRepo.refreshRecipes()
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRecipeClicked(entity: RecipeSummaryEntity) {
|
private fun onRecipeClicked(entity: RecipeSummaryEntity) {
|
||||||
@@ -75,23 +80,23 @@ internal class RecipesListViewModel @Inject constructor(
|
|||||||
private fun onFavoriteIconClick(recipeSummaryEntity: RecipeSummaryEntity) {
|
private fun onFavoriteIconClick(recipeSummaryEntity: RecipeSummaryEntity) {
|
||||||
logger.v { "onFavoriteIconClick() called with: recipeSummaryEntity = $recipeSummaryEntity" }
|
logger.v { "onFavoriteIconClick() called with: recipeSummaryEntity = $recipeSummaryEntity" }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = recipeRepo.updateIsRecipeFavorite(
|
val result =
|
||||||
recipeSlug = recipeSummaryEntity.slug,
|
recipeRepo.updateIsRecipeFavorite(
|
||||||
isFavorite = recipeSummaryEntity.isFavorite.not(),
|
recipeSlug = recipeSummaryEntity.slug,
|
||||||
)
|
isFavorite = recipeSummaryEntity.isFavorite.not(),
|
||||||
val snackbar = result.fold(
|
)
|
||||||
onSuccess = { isFavorite ->
|
val snackbar =
|
||||||
val name = recipeSummaryEntity.name
|
result.fold(
|
||||||
if (isFavorite) {
|
onSuccess = { _ ->
|
||||||
RecipeListSnackbar.FavoriteAdded(name)
|
val name = recipeSummaryEntity.name
|
||||||
} else {
|
if (recipeSummaryEntity.isFavorite) {
|
||||||
RecipeListSnackbar.FavoriteRemoved(name)
|
RecipeListSnackbar.FavoriteRemoved(name)
|
||||||
}
|
} else {
|
||||||
},
|
RecipeListSnackbar.FavoriteAdded(name)
|
||||||
onFailure = {
|
}
|
||||||
RecipeListSnackbar.FavoriteUpdateFailed
|
},
|
||||||
}
|
onFailure = { RecipeListSnackbar.FavoriteUpdateFailed }
|
||||||
)
|
)
|
||||||
_screenState.update { it.copy(snackbarState = snackbar) }
|
_screenState.update { it.copy(snackbarState = snackbar) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,10 +106,11 @@ internal class RecipesListViewModel @Inject constructor(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = recipeRepo.deleteRecipe(recipeSummaryEntity)
|
val result = recipeRepo.deleteRecipe(recipeSummaryEntity)
|
||||||
logger.d { "onDeleteConfirm: delete result is $result" }
|
logger.d { "onDeleteConfirm: delete result is $result" }
|
||||||
val snackbar = result.fold(
|
val snackbar =
|
||||||
onSuccess = { null },
|
result.fold(
|
||||||
onFailure = { RecipeListSnackbar.DeleteFailed },
|
onSuccess = { null },
|
||||||
)
|
onFailure = { RecipeListSnackbar.DeleteFailed },
|
||||||
|
)
|
||||||
_screenState.update { it.copy(snackbarState = snackbar) }
|
_screenState.update { it.copy(snackbarState = snackbar) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,10 +144,10 @@ internal class RecipesListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal data class RecipeListState(
|
internal data class RecipeListState(
|
||||||
val pagingDataRecipeState: Flow<PagingData<RecipeListItemState>>,
|
val pagingDataRecipeState: Flow<PagingData<RecipeListItemState>>,
|
||||||
val snackbarState: RecipeListSnackbar? = null,
|
val snackbarState: RecipeListSnackbar? = null,
|
||||||
val recipeIdToOpen: String? = null,
|
val recipeIdToOpen: String? = null,
|
||||||
val searchQuery: String = "",
|
val searchQuery: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
internal sealed interface RecipeListEvent {
|
internal sealed interface RecipeListEvent {
|
||||||
@@ -157,4 +163,4 @@ internal sealed interface RecipeListEvent {
|
|||||||
data object SnackbarShown : RecipeListEvent
|
data object SnackbarShown : RecipeListEvent
|
||||||
|
|
||||||
data class SearchQueryChanged(val query: String) : RecipeListEvent
|
data class SearchQueryChanged(val query: String) : RecipeListEvent
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,11 @@ internal fun SearchTextField(
|
|||||||
focusedIndicatorColor = Color.Transparent,
|
focusedIndicatorColor = Color.Transparent,
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
disabledIndicatorColor = 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,
|
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")
|
@Query("SELECT * FROM recipe_summaries ORDER BY recipe_summaries_date_added DESC")
|
||||||
fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity>
|
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>
|
fun queryRecipesByPages(query: String): PagingSource<Int, RecipeSummaryEntity>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertRecipeSummaries(recipeSummaryEntity: Iterable<RecipeSummaryEntity>)
|
suspend fun insertRecipeSummaries(recipeSummaryEntity: Iterable<RecipeSummaryEntity>)
|
||||||
|
|
||||||
@Transaction
|
@Transaction @Query("DELETE FROM recipe_summaries") suspend fun removeAllRecipes()
|
||||||
@Query("DELETE FROM recipe_summaries")
|
|
||||||
suspend fun removeAllRecipes()
|
|
||||||
|
|
||||||
@Query("SELECT * FROM recipe_summaries ORDER BY recipe_summaries_date_added DESC")
|
@Query("SELECT * FROM recipe_summaries ORDER BY recipe_summaries_date_added DESC")
|
||||||
suspend fun queryAllRecipes(): List<RecipeSummaryEntity>
|
suspend fun queryAllRecipes(): List<RecipeSummaryEntity>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertRecipe(recipe: RecipeEntity)
|
||||||
suspend fun insertRecipe(recipe: RecipeEntity)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertRecipes(recipe: List<RecipeEntity>)
|
suspend fun insertRecipes(recipe: List<RecipeEntity>)
|
||||||
@@ -36,19 +35,25 @@ internal interface RecipeDao {
|
|||||||
suspend fun insertRecipeIngredients(ingredients: List<RecipeIngredientEntity>)
|
suspend fun insertRecipeIngredients(ingredients: List<RecipeIngredientEntity>)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertIngredientToInstructionEntities(entities: List<RecipeIngredientToInstructionEntity>)
|
suspend fun insertIngredientToInstructionEntities(
|
||||||
|
entities: List<RecipeIngredientToInstructionEntity>
|
||||||
|
)
|
||||||
|
|
||||||
@Transaction
|
@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(
|
@Query(
|
||||||
"SELECT * FROM recipe " +
|
"SELECT * FROM recipe " +
|
||||||
"JOIN recipe_summaries USING(recipe_id) " +
|
"JOIN recipe_summaries USING(recipe_id) " +
|
||||||
"LEFT JOIN recipe_ingredient USING(recipe_id) " +
|
"LEFT JOIN recipe_ingredient USING(recipe_id) " +
|
||||||
"LEFT JOIN recipe_instruction USING(recipe_id) " +
|
"LEFT JOIN recipe_instruction USING(recipe_id) " +
|
||||||
"LEFT JOIN recipe_ingredient_to_instruction USING(recipe_id) " +
|
"LEFT JOIN recipe_ingredient_to_instruction USING(recipe_id) " +
|
||||||
"WHERE recipe.recipe_id = :recipeId"
|
"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)")
|
@Query("DELETE FROM recipe_ingredient WHERE recipe_id IN (:recipeIds)")
|
||||||
suspend fun deleteRecipeIngredients(vararg recipeIds: String)
|
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)")
|
@Query("DELETE FROM recipe_ingredient_to_instruction WHERE recipe_id IN (:recipeIds)")
|
||||||
suspend fun deleteRecipeIngredientToInstructions(vararg recipeIds: String)
|
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>)
|
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>)
|
suspend fun setNonFavorite(favorites: List<String>)
|
||||||
|
|
||||||
@Delete
|
@Delete suspend fun deleteRecipe(entity: RecipeSummaryEntity)
|
||||||
suspend fun deleteRecipe(entity: RecipeSummaryEntity)
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import com.atridad.mealient.database.recipe.entity.RecipeWithSummaryAndIngredien
|
|||||||
import com.atridad.mealient.logging.Logger
|
import com.atridad.mealient.logging.Logger
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class RecipeStorageImpl @Inject constructor(
|
internal class RecipeStorageImpl
|
||||||
private val db: AppDb,
|
@Inject
|
||||||
private val logger: Logger,
|
constructor(
|
||||||
private val recipeDao: RecipeDao,
|
private val db: AppDb,
|
||||||
|
private val logger: Logger,
|
||||||
|
private val recipeDao: RecipeDao,
|
||||||
) : RecipeStorage {
|
) : RecipeStorage {
|
||||||
|
|
||||||
override suspend fun saveRecipes(recipes: List<RecipeSummaryEntity>) {
|
override suspend fun saveRecipes(recipes: List<RecipeSummaryEntity>) {
|
||||||
@@ -43,12 +45,14 @@ internal class RecipeStorageImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveRecipeInfo(
|
override suspend fun saveRecipeInfo(
|
||||||
recipe: RecipeEntity,
|
recipe: RecipeEntity,
|
||||||
ingredients: List<RecipeIngredientEntity>,
|
ingredients: List<RecipeIngredientEntity>,
|
||||||
instructions: List<RecipeInstructionEntity>,
|
instructions: List<RecipeInstructionEntity>,
|
||||||
ingredientToInstruction: List<RecipeIngredientToInstructionEntity>,
|
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 {
|
db.withTransaction {
|
||||||
recipeDao.insertRecipe(recipe)
|
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" }
|
logger.v { "queryRecipeInfo() called with: recipeId = $recipeId" }
|
||||||
val fullRecipeInfo = recipeDao.queryFullRecipeInfo(recipeId)
|
val fullRecipeInfo = recipeDao.queryFullRecipeInfo(recipeId)
|
||||||
logger.v { "queryRecipeInfo() returned: $fullRecipeInfo" }
|
logger.v { "queryRecipeInfo() returned: $fullRecipeInfo" }
|
||||||
@@ -73,8 +79,12 @@ internal class RecipeStorageImpl @Inject constructor(
|
|||||||
override suspend fun updateFavoriteRecipes(favorites: List<String>) {
|
override suspend fun updateFavoriteRecipes(favorites: List<String>) {
|
||||||
logger.v { "updateFavoriteRecipes() called with: favorites = $favorites" }
|
logger.v { "updateFavoriteRecipes() called with: favorites = $favorites" }
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
recipeDao.setFavorite(favorites)
|
if (favorites.isNotEmpty()) {
|
||||||
recipeDao.setNonFavorite(favorites)
|
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" }
|
logger.v { "deleteRecipeBySlug() called with: entity = $entity" }
|
||||||
recipeDao.deleteRecipe(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.GetShoppingListResponse
|
||||||
import com.atridad.mealient.datasource.models.GetShoppingListsResponse
|
import com.atridad.mealient.datasource.models.GetShoppingListsResponse
|
||||||
import com.atridad.mealient.datasource.models.GetUnitsResponse
|
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.GetUserInfoResponse
|
||||||
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
|
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
|
||||||
import com.atridad.mealient.datasource.models.UpdateRecipeRequest
|
import com.atridad.mealient.datasource.models.UpdateRecipeRequest
|
||||||
@@ -20,39 +21,37 @@ import com.atridad.mealient.datasource.models.VersionResponse
|
|||||||
interface MealieDataSource {
|
interface MealieDataSource {
|
||||||
|
|
||||||
suspend fun createRecipe(
|
suspend fun createRecipe(
|
||||||
recipe: CreateRecipeRequest,
|
recipe: CreateRecipeRequest,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
suspend fun updateRecipe(
|
suspend fun updateRecipe(
|
||||||
slug: String,
|
slug: String,
|
||||||
recipe: UpdateRecipeRequest,
|
recipe: UpdateRecipeRequest,
|
||||||
): GetRecipeResponse
|
): GetRecipeResponse
|
||||||
|
|
||||||
/**
|
/** Tries to acquire authentication token using the provided credentials */
|
||||||
* Tries to acquire authentication token using the provided credentials
|
|
||||||
*/
|
|
||||||
suspend fun authenticate(
|
suspend fun authenticate(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
suspend fun getVersionInfo(baseURL: String): VersionResponse
|
suspend fun getVersionInfo(baseURL: String): VersionResponse
|
||||||
|
|
||||||
suspend fun requestRecipes(
|
suspend fun requestRecipes(
|
||||||
page: Int,
|
page: Int,
|
||||||
perPage: Int,
|
perPage: Int,
|
||||||
): List<GetRecipeSummaryResponse>
|
): List<GetRecipeSummaryResponse>
|
||||||
|
|
||||||
suspend fun requestRecipeInfo(
|
suspend fun requestRecipeInfo(
|
||||||
slug: String,
|
slug: String,
|
||||||
): GetRecipeResponse
|
): GetRecipeResponse
|
||||||
|
|
||||||
suspend fun parseRecipeFromURL(
|
suspend fun parseRecipeFromURL(
|
||||||
request: ParseRecipeURLRequest,
|
request: ParseRecipeURLRequest,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
suspend fun createApiToken(
|
suspend fun createApiToken(
|
||||||
request: CreateApiTokenRequest,
|
request: CreateApiTokenRequest,
|
||||||
): CreateApiTokenResponse
|
): CreateApiTokenResponse
|
||||||
|
|
||||||
suspend fun requestUserInfo(): GetUserInfoResponse
|
suspend fun requestUserInfo(): GetUserInfoResponse
|
||||||
@@ -82,4 +81,6 @@ interface MealieDataSource {
|
|||||||
suspend fun deleteShoppingList(id: String)
|
suspend fun deleteShoppingList(id: String)
|
||||||
|
|
||||||
suspend fun updateShoppingListName(id: String, name: 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.GetShoppingListsResponse
|
||||||
import com.atridad.mealient.datasource.models.GetTokenResponse
|
import com.atridad.mealient.datasource.models.GetTokenResponse
|
||||||
import com.atridad.mealient.datasource.models.GetUnitsResponse
|
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.GetUserInfoResponse
|
||||||
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
|
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
|
||||||
import com.atridad.mealient.datasource.models.UpdateRecipeRequest
|
import com.atridad.mealient.datasource.models.UpdateRecipeRequest
|
||||||
@@ -25,8 +26,8 @@ internal interface MealieService {
|
|||||||
suspend fun createRecipe(addRecipeRequest: CreateRecipeRequest): String
|
suspend fun createRecipe(addRecipeRequest: CreateRecipeRequest): String
|
||||||
|
|
||||||
suspend fun updateRecipe(
|
suspend fun updateRecipe(
|
||||||
addRecipeRequest: UpdateRecipeRequest,
|
addRecipeRequest: UpdateRecipeRequest,
|
||||||
slug: String,
|
slug: String,
|
||||||
): GetRecipeResponse
|
): GetRecipeResponse
|
||||||
|
|
||||||
suspend fun getVersion(baseURL: String): VersionResponse
|
suspend fun getVersion(baseURL: String): VersionResponse
|
||||||
@@ -68,6 +69,8 @@ internal interface MealieService {
|
|||||||
suspend fun deleteShoppingList(id: String)
|
suspend fun deleteShoppingList(id: String)
|
||||||
|
|
||||||
suspend fun updateShoppingList(id: String, request: JsonElement)
|
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.GetShoppingListResponse
|
||||||
import com.atridad.mealient.datasource.models.GetShoppingListsResponse
|
import com.atridad.mealient.datasource.models.GetShoppingListsResponse
|
||||||
import com.atridad.mealient.datasource.models.GetUnitsResponse
|
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.GetUserInfoResponse
|
||||||
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
|
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
|
||||||
import com.atridad.mealient.datasource.models.UpdateRecipeRequest
|
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.NoTransformationFoundException
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.plugins.ResponseException
|
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.JsonElement
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import java.net.SocketException
|
|
||||||
import java.net.SocketTimeoutException
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class MealieDataSourceImpl @Inject constructor(
|
internal class MealieDataSourceImpl
|
||||||
private val networkRequestWrapper: NetworkRequestWrapper,
|
@Inject
|
||||||
private val service: MealieService,
|
constructor(
|
||||||
|
private val networkRequestWrapper: NetworkRequestWrapper,
|
||||||
|
private val service: MealieService,
|
||||||
) : MealieDataSource {
|
) : MealieDataSource {
|
||||||
|
|
||||||
override suspend fun createRecipe(
|
override suspend fun createRecipe(
|
||||||
recipe: CreateRecipeRequest,
|
recipe: CreateRecipeRequest,
|
||||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): String =
|
||||||
block = { service.createRecipe(recipe) },
|
networkRequestWrapper
|
||||||
logMethod = { "createRecipe" },
|
.makeCallAndHandleUnauthorized(
|
||||||
logParameters = { "recipe = $recipe" }
|
block = { service.createRecipe(recipe) },
|
||||||
).trim('"')
|
logMethod = { "createRecipe" },
|
||||||
|
logParameters = { "recipe = $recipe" }
|
||||||
|
)
|
||||||
|
.trim('"')
|
||||||
|
|
||||||
override suspend fun updateRecipe(
|
override suspend fun updateRecipe(
|
||||||
slug: String,
|
slug: String,
|
||||||
recipe: UpdateRecipeRequest,
|
recipe: UpdateRecipeRequest,
|
||||||
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): GetRecipeResponse =
|
||||||
block = { service.updateRecipe(recipe, slug) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "updateRecipe" },
|
block = { service.updateRecipe(recipe, slug) },
|
||||||
logParameters = { "slug = $slug, recipe = $recipe" }
|
logMethod = { "updateRecipe" },
|
||||||
)
|
logParameters = { "slug = $slug, recipe = $recipe" }
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun authenticate(
|
override suspend fun authenticate(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
): String = networkRequestWrapper.makeCall(
|
): String =
|
||||||
block = { service.getToken(username, password) },
|
networkRequestWrapper
|
||||||
logMethod = { "authenticate" },
|
.makeCall(
|
||||||
logParameters = { "username = $username, password = $password" }
|
block = { service.getToken(username, password) },
|
||||||
).map { it.accessToken }.getOrElse {
|
logMethod = { "authenticate" },
|
||||||
val errorDetail = (it as? ResponseException)?.response?.body<ErrorDetail>() ?: throw it
|
logParameters = { "username = $username, password = $password" }
|
||||||
throw if (errorDetail.detail == "Unauthorized") NetworkError.Unauthorized(it) else it
|
)
|
||||||
}
|
.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 =
|
override suspend fun getVersionInfo(baseURL: String): VersionResponse =
|
||||||
networkRequestWrapper.makeCall(
|
networkRequestWrapper.makeCall(
|
||||||
block = { service.getVersion(baseURL) },
|
block = { service.getVersion(baseURL) },
|
||||||
logMethod = { "getVersionInfo" },
|
logMethod = { "getVersionInfo" },
|
||||||
logParameters = { "baseURL = $baseURL" }
|
logParameters = { "baseURL = $baseURL" }
|
||||||
).getOrElse {
|
)
|
||||||
throw when (it) {
|
.getOrElse {
|
||||||
is ResponseException, is NoTransformationFoundException -> NetworkError.NotMealie(it)
|
throw when (it) {
|
||||||
is SocketTimeoutException, is SocketException -> NetworkError.NoServerConnection(it)
|
is ResponseException, is NoTransformationFoundException ->
|
||||||
else -> NetworkError.MalformedUrl(it)
|
NetworkError.NotMealie(it)
|
||||||
}
|
is SocketTimeoutException, is SocketException ->
|
||||||
}
|
NetworkError.NoServerConnection(it)
|
||||||
|
else -> NetworkError.MalformedUrl(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun requestRecipes(
|
override suspend fun requestRecipes(
|
||||||
page: Int,
|
page: Int,
|
||||||
perPage: Int,
|
perPage: Int,
|
||||||
): List<GetRecipeSummaryResponse> = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): List<GetRecipeSummaryResponse> {
|
||||||
block = { service.getRecipeSummary(page, perPage) },
|
val response =
|
||||||
logMethod = { "requestRecipes" },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logParameters = { "page = $page, perPage = $perPage" }
|
block = { service.getRecipeSummary(page, perPage) },
|
||||||
).items
|
logMethod = { "requestRecipes" },
|
||||||
|
logParameters = { "page = $page, perPage = $perPage" }
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.items
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun requestRecipeInfo(
|
override suspend fun requestRecipeInfo(
|
||||||
slug: String,
|
slug: String,
|
||||||
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): GetRecipeResponse =
|
||||||
block = { service.getRecipe(slug) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "requestRecipeInfo" },
|
block = { service.getRecipe(slug) },
|
||||||
logParameters = { "slug = $slug" }
|
logMethod = { "requestRecipeInfo" },
|
||||||
)
|
logParameters = { "slug = $slug" }
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun parseRecipeFromURL(
|
override suspend fun parseRecipeFromURL(
|
||||||
request: ParseRecipeURLRequest,
|
request: ParseRecipeURLRequest,
|
||||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): String =
|
||||||
block = { service.createRecipeFromURL(request) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "parseRecipeFromURL" },
|
block = { service.createRecipeFromURL(request) },
|
||||||
logParameters = { "request = $request" }
|
logMethod = { "parseRecipeFromURL" },
|
||||||
)
|
logParameters = { "request = $request" }
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun createApiToken(
|
override suspend fun createApiToken(
|
||||||
request: CreateApiTokenRequest,
|
request: CreateApiTokenRequest,
|
||||||
): CreateApiTokenResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): CreateApiTokenResponse =
|
||||||
block = { service.createApiToken(request) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "createApiToken" },
|
block = { service.createApiToken(request) },
|
||||||
logParameters = { "request = $request" }
|
logMethod = { "createApiToken" },
|
||||||
)
|
logParameters = { "request = $request" }
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun requestUserInfo(): GetUserInfoResponse {
|
override suspend fun requestUserInfo(): GetUserInfoResponse {
|
||||||
return networkRequestWrapper.makeCallAndHandleUnauthorized(
|
val response =
|
||||||
block = { service.getUserSelfInfo() },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "requestUserInfo" },
|
block = { service.getUserSelfInfo() },
|
||||||
)
|
logMethod = { "requestUserInfo" },
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun removeFavoriteRecipe(
|
override suspend fun removeFavoriteRecipe(
|
||||||
userId: String,
|
userId: String,
|
||||||
recipeSlug: String,
|
recipeSlug: String,
|
||||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): Unit {
|
||||||
block = { service.removeFavoriteRecipe(userId, recipeSlug) },
|
|
||||||
logMethod = { "removeFavoriteRecipe" },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
block = { service.removeFavoriteRecipe(userId, recipeSlug) },
|
||||||
)
|
logMethod = { "removeFavoriteRecipe" },
|
||||||
|
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun addFavoriteRecipe(
|
override suspend fun addFavoriteRecipe(
|
||||||
userId: String,
|
userId: String,
|
||||||
recipeSlug: String,
|
recipeSlug: String,
|
||||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): Unit {
|
||||||
block = { service.addFavoriteRecipe(userId, recipeSlug) },
|
|
||||||
logMethod = { "addFavoriteRecipe" },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
block = { service.addFavoriteRecipe(userId, recipeSlug) },
|
||||||
)
|
logMethod = { "addFavoriteRecipe" },
|
||||||
|
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun deleteRecipe(
|
override suspend fun deleteRecipe(
|
||||||
slug: String,
|
slug: String,
|
||||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): Unit =
|
||||||
block = { service.deleteRecipe(slug) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "deleteRecipe" },
|
block = { service.deleteRecipe(slug) },
|
||||||
logParameters = { "slug = $slug" }
|
logMethod = { "deleteRecipe" },
|
||||||
)
|
logParameters = { "slug = $slug" }
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun getShoppingLists(
|
override suspend fun getShoppingLists(
|
||||||
page: Int,
|
page: Int,
|
||||||
perPage: Int,
|
perPage: Int,
|
||||||
): GetShoppingListsResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): GetShoppingListsResponse =
|
||||||
block = { service.getShoppingLists(page, perPage) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "getShoppingLists" },
|
block = { service.getShoppingLists(page, perPage) },
|
||||||
logParameters = { "page = $page, perPage = $perPage" }
|
logMethod = { "getShoppingLists" },
|
||||||
)
|
logParameters = { "page = $page, perPage = $perPage" }
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun getShoppingList(
|
override suspend fun getShoppingList(
|
||||||
id: String,
|
id: String,
|
||||||
): GetShoppingListResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): GetShoppingListResponse =
|
||||||
block = { service.getShoppingList(id) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "getShoppingList" },
|
block = { service.getShoppingList(id) },
|
||||||
logParameters = { "id = $id" }
|
logMethod = { "getShoppingList" },
|
||||||
)
|
logParameters = { "id = $id" }
|
||||||
|
)
|
||||||
|
|
||||||
private suspend fun getShoppingListItem(
|
private suspend fun getShoppingListItem(
|
||||||
id: String,
|
id: String,
|
||||||
): JsonElement = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): JsonElement =
|
||||||
block = { service.getShoppingListItem(id) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "getShoppingListItem" },
|
block = { service.getShoppingListItem(id) },
|
||||||
logParameters = { "id = $id" }
|
logMethod = { "getShoppingListItem" },
|
||||||
)
|
logParameters = { "id = $id" }
|
||||||
|
)
|
||||||
|
|
||||||
private suspend fun updateShoppingListItem(
|
private suspend fun updateShoppingListItem(
|
||||||
id: String,
|
id: String,
|
||||||
request: JsonElement,
|
request: JsonElement,
|
||||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
) =
|
||||||
block = { service.updateShoppingListItem(id, request) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "updateShoppingListItem" },
|
block = { service.updateShoppingListItem(id, request) },
|
||||||
logParameters = { "id = $id, request = $request" }
|
logMethod = { "updateShoppingListItem" },
|
||||||
)
|
logParameters = { "id = $id, request = $request" }
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun deleteShoppingListItem(
|
override suspend fun deleteShoppingListItem(
|
||||||
id: String,
|
id: String,
|
||||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
) =
|
||||||
block = { service.deleteShoppingListItem(id) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "deleteShoppingListItem" },
|
block = { service.deleteShoppingListItem(id) },
|
||||||
logParameters = { "id = $id" }
|
logMethod = { "deleteShoppingListItem" },
|
||||||
)
|
logParameters = { "id = $id" }
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun updateShoppingListItem(
|
override suspend fun updateShoppingListItem(
|
||||||
item: GetShoppingListItemResponse,
|
item: GetShoppingListItemResponse,
|
||||||
) {
|
) {
|
||||||
// Has to be done in two steps because we can't specify only the changed fields
|
// Has to be done in two steps because we can't specify only the changed fields
|
||||||
val remoteItem = getShoppingListItem(item.id)
|
val remoteItem = getShoppingListItem(item.id)
|
||||||
val updatedItem = remoteItem.jsonObject.toMutableMap().apply {
|
val updatedItem =
|
||||||
put("checked", JsonPrimitive(item.checked))
|
remoteItem.jsonObject.toMutableMap().apply {
|
||||||
put("isFood", JsonPrimitive(item.isFood))
|
put("checked", JsonPrimitive(item.checked))
|
||||||
put("note", JsonPrimitive(item.note))
|
put("isFood", JsonPrimitive(item.isFood))
|
||||||
put("quantity", JsonPrimitive(item.quantity))
|
put("note", JsonPrimitive(item.note))
|
||||||
put("foodId", JsonPrimitive(item.food?.id))
|
put("quantity", JsonPrimitive(item.quantity))
|
||||||
put("unitId", JsonPrimitive(item.unit?.id))
|
put("foodId", JsonPrimitive(item.food?.id))
|
||||||
remove("unit")
|
put("unitId", JsonPrimitive(item.unit?.id))
|
||||||
remove("food")
|
remove("unit")
|
||||||
}
|
remove("food")
|
||||||
|
}
|
||||||
updateShoppingListItem(item.id, JsonObject(updatedItem))
|
updateShoppingListItem(item.id, JsonObject(updatedItem))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getFoods(): GetFoodsResponse {
|
override suspend fun getFoods(): GetFoodsResponse {
|
||||||
return networkRequestWrapper.makeCallAndHandleUnauthorized(
|
return networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.getFoods(perPage = -1) },
|
block = { service.getFoods(perPage = -1) },
|
||||||
logMethod = { "getFoods" },
|
logMethod = { "getFoods" },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUnits(): GetUnitsResponse {
|
override suspend fun getUnits(): GetUnitsResponse {
|
||||||
return networkRequestWrapper.makeCallAndHandleUnauthorized(
|
return networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.getUnits(perPage = -1) },
|
block = { service.getUnits(perPage = -1) },
|
||||||
logMethod = { "getUnits" },
|
logMethod = { "getUnits" },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun addShoppingListItem(
|
override suspend fun addShoppingListItem(
|
||||||
request: CreateShoppingListItemRequest,
|
request: CreateShoppingListItemRequest,
|
||||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
) =
|
||||||
block = { service.createShoppingListItem(request) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "addShoppingListItem" },
|
block = { service.createShoppingListItem(request) },
|
||||||
logParameters = { "request = $request" }
|
logMethod = { "addShoppingListItem" },
|
||||||
)
|
logParameters = { "request = $request" }
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun addShoppingList(
|
override suspend fun addShoppingList(
|
||||||
request: CreateShoppingListRequest,
|
request: CreateShoppingListRequest,
|
||||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
) =
|
||||||
block = { service.createShoppingList(request) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "createShoppingList" },
|
block = { service.createShoppingList(request) },
|
||||||
logParameters = { "request = $request" }
|
logMethod = { "createShoppingList" },
|
||||||
)
|
logParameters = { "request = $request" }
|
||||||
|
)
|
||||||
|
|
||||||
private suspend fun updateShoppingList(
|
private suspend fun updateShoppingList(
|
||||||
id: String,
|
id: String,
|
||||||
request: JsonElement,
|
request: JsonElement,
|
||||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
) =
|
||||||
block = { service.updateShoppingList(id, request) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "updateShoppingList" },
|
block = { service.updateShoppingList(id, request) },
|
||||||
logParameters = { "id = $id, request = $request" }
|
logMethod = { "updateShoppingList" },
|
||||||
)
|
logParameters = { "id = $id, request = $request" }
|
||||||
|
)
|
||||||
|
|
||||||
private suspend fun getShoppingListJson(
|
private suspend fun getShoppingListJson(
|
||||||
id: String,
|
id: String,
|
||||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
) =
|
||||||
block = { service.getShoppingListJson(id) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "getShoppingListJson" },
|
block = { service.getShoppingListJson(id) },
|
||||||
logParameters = { "id = $id" }
|
logMethod = { "getShoppingListJson" },
|
||||||
)
|
logParameters = { "id = $id" }
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun deleteShoppingList(
|
override suspend fun deleteShoppingList(
|
||||||
id: String,
|
id: String,
|
||||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
) =
|
||||||
block = { service.deleteShoppingList(id) },
|
networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
logMethod = { "deleteShoppingList" },
|
block = { service.deleteShoppingList(id) },
|
||||||
logParameters = { "id = $id" }
|
logMethod = { "deleteShoppingList" },
|
||||||
)
|
logParameters = { "id = $id" }
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun updateShoppingListName(
|
override suspend fun updateShoppingListName(id: String, name: String) {
|
||||||
id: String,
|
|
||||||
name: String
|
|
||||||
) {
|
|
||||||
// Has to be done in two steps because we can't specify only the changed fields
|
// Has to be done in two steps because we can't specify only the changed fields
|
||||||
val remoteItem = getShoppingListJson(id)
|
val remoteItem = getShoppingListJson(id)
|
||||||
val updatedItem = remoteItem.jsonObject.toMutableMap().apply {
|
val updatedItem =
|
||||||
put("name", JsonPrimitive(name))
|
remoteItem
|
||||||
}.let(::JsonObject)
|
.jsonObject
|
||||||
|
.toMutableMap()
|
||||||
|
.apply { put("name", JsonPrimitive(name)) }
|
||||||
|
.let(::JsonObject)
|
||||||
updateShoppingList(id, updatedItem)
|
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.GetShoppingListsResponse
|
||||||
import com.atridad.mealient.datasource.models.GetTokenResponse
|
import com.atridad.mealient.datasource.models.GetTokenResponse
|
||||||
import com.atridad.mealient.datasource.models.GetUnitsResponse
|
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.GetUserInfoResponse
|
||||||
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
|
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
|
||||||
import com.atridad.mealient.datasource.models.UpdateRecipeRequest
|
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.parameters
|
||||||
import io.ktor.http.path
|
import io.ktor.http.path
|
||||||
import io.ktor.http.takeFrom
|
import io.ktor.http.takeFrom
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
|
||||||
internal class MealieServiceKtor @Inject constructor(
|
internal class MealieServiceKtor
|
||||||
private val httpClient: HttpClient,
|
@Inject
|
||||||
private val serverUrlProviderProvider: Provider<ServerUrlProvider>,
|
constructor(
|
||||||
|
private val httpClient: HttpClient,
|
||||||
|
private val serverUrlProviderProvider: Provider<ServerUrlProvider>,
|
||||||
) : MealieService {
|
) : MealieService {
|
||||||
|
|
||||||
private val serverUrlProvider: ServerUrlProvider
|
private val serverUrlProvider: ServerUrlProvider
|
||||||
@@ -52,111 +55,109 @@ internal class MealieServiceKtor @Inject constructor(
|
|||||||
append("password", password)
|
append("password", password)
|
||||||
}
|
}
|
||||||
|
|
||||||
return httpClient.post {
|
return httpClient
|
||||||
endpoint("/api/auth/token")
|
.post {
|
||||||
setBody(FormDataContent(formParameters))
|
endpoint("/api/auth/token")
|
||||||
}.body()
|
setBody(FormDataContent(formParameters))
|
||||||
|
}
|
||||||
|
.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createRecipe(addRecipeRequest: CreateRecipeRequest): String {
|
override suspend fun createRecipe(addRecipeRequest: CreateRecipeRequest): String {
|
||||||
return httpClient.post {
|
return httpClient
|
||||||
endpoint("/api/recipes")
|
.post {
|
||||||
contentType(ContentType.Application.Json)
|
endpoint("/api/recipes")
|
||||||
setBody(addRecipeRequest)
|
contentType(ContentType.Application.Json)
|
||||||
}.body()
|
setBody(addRecipeRequest)
|
||||||
|
}
|
||||||
|
.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateRecipe(
|
override suspend fun updateRecipe(
|
||||||
addRecipeRequest: UpdateRecipeRequest,
|
addRecipeRequest: UpdateRecipeRequest,
|
||||||
slug: String,
|
slug: String,
|
||||||
): GetRecipeResponse {
|
): GetRecipeResponse {
|
||||||
return httpClient.patch {
|
return httpClient
|
||||||
endpoint("/api/recipes/$slug")
|
.patch {
|
||||||
contentType(ContentType.Application.Json)
|
endpoint("/api/recipes/$slug")
|
||||||
setBody(addRecipeRequest)
|
contentType(ContentType.Application.Json)
|
||||||
}.body()
|
setBody(addRecipeRequest)
|
||||||
|
}
|
||||||
|
.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getVersion(baseURL: String): VersionResponse {
|
override suspend fun getVersion(baseURL: String): VersionResponse {
|
||||||
return httpClient.get {
|
return httpClient.get { endpoint(baseURL, "/api/app/about") }.body()
|
||||||
endpoint(baseURL, "/api/app/about")
|
|
||||||
}.body()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRecipeSummary(page: Int, perPage: Int): GetRecipesResponse {
|
override suspend fun getRecipeSummary(page: Int, perPage: Int): GetRecipesResponse {
|
||||||
return httpClient.get {
|
return httpClient
|
||||||
endpoint("/api/recipes") {
|
.get {
|
||||||
parameters.append("page", page.toString())
|
endpoint("/api/recipes") {
|
||||||
parameters.append("perPage", perPage.toString())
|
parameters.append("page", page.toString())
|
||||||
}
|
parameters.append("perPage", perPage.toString())
|
||||||
}.body()
|
}
|
||||||
|
}
|
||||||
|
.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRecipe(slug: String): GetRecipeResponse {
|
override suspend fun getRecipe(slug: String): GetRecipeResponse {
|
||||||
return httpClient.get {
|
return httpClient.get { endpoint("/api/recipes/$slug") }.body()
|
||||||
endpoint("/api/recipes/$slug")
|
|
||||||
}.body()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createRecipeFromURL(request: ParseRecipeURLRequest): String {
|
override suspend fun createRecipeFromURL(request: ParseRecipeURLRequest): String {
|
||||||
return httpClient.post {
|
return httpClient
|
||||||
endpoint("/api/recipes/create-url")
|
.post {
|
||||||
contentType(ContentType.Application.Json)
|
endpoint("/api/recipes/create-url")
|
||||||
setBody(request)
|
contentType(ContentType.Application.Json)
|
||||||
}.body()
|
setBody(request)
|
||||||
|
}
|
||||||
|
.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createApiToken(request: CreateApiTokenRequest): CreateApiTokenResponse {
|
override suspend fun createApiToken(request: CreateApiTokenRequest): CreateApiTokenResponse {
|
||||||
return httpClient.post {
|
return httpClient
|
||||||
endpoint("/api/users/api-tokens")
|
.post {
|
||||||
contentType(ContentType.Application.Json)
|
endpoint("/api/users/api-tokens")
|
||||||
setBody(request)
|
contentType(ContentType.Application.Json)
|
||||||
}.body()
|
setBody(request)
|
||||||
|
}
|
||||||
|
.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUserSelfInfo(): GetUserInfoResponse {
|
override suspend fun getUserSelfInfo(): GetUserInfoResponse {
|
||||||
return httpClient.get {
|
return httpClient.get { endpoint("/api/users/self") }.body()
|
||||||
endpoint("/api/users/self")
|
|
||||||
}.body()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String) {
|
override suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String) {
|
||||||
httpClient.delete {
|
httpClient.delete { endpoint("/api/users/$userId/favorites/$recipeSlug") }
|
||||||
endpoint("/api/users/$userId/favorites/$recipeSlug")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun addFavoriteRecipe(userId: String, recipeSlug: String) {
|
override suspend fun addFavoriteRecipe(userId: String, recipeSlug: String) {
|
||||||
httpClient.post {
|
httpClient.post { endpoint("/api/users/$userId/favorites/$recipeSlug") }
|
||||||
endpoint("/api/users/$userId/favorites/$recipeSlug")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteRecipe(slug: String) {
|
override suspend fun deleteRecipe(slug: String) {
|
||||||
httpClient.delete {
|
httpClient.delete { endpoint("/api/recipes/$slug") }
|
||||||
endpoint("/api/recipes/$slug")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponse {
|
override suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponse {
|
||||||
return httpClient.get {
|
return httpClient
|
||||||
endpoint("/api/households/shopping/lists") {
|
.get {
|
||||||
parameters.append("page", page.toString())
|
endpoint("/api/households/shopping/lists") {
|
||||||
parameters.append("perPage", perPage.toString())
|
parameters.append("page", page.toString())
|
||||||
}
|
parameters.append("perPage", perPage.toString())
|
||||||
}.body()
|
}
|
||||||
|
}
|
||||||
|
.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getShoppingList(id: String): GetShoppingListResponse {
|
override suspend fun getShoppingList(id: String): GetShoppingListResponse {
|
||||||
return httpClient.get {
|
return httpClient.get { endpoint("/api/households/shopping/lists/$id") }.body()
|
||||||
endpoint("/api/households/shopping/lists/$id")
|
|
||||||
}.body()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getShoppingListItem(id: String): JsonElement {
|
override suspend fun getShoppingListItem(id: String): JsonElement {
|
||||||
return httpClient.get {
|
return httpClient.get { endpoint("/api/households/shopping/items/$id") }.body()
|
||||||
endpoint("/api/households/shopping/items/$id")
|
|
||||||
}.body()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateShoppingListItem(id: String, request: JsonElement) {
|
override suspend fun updateShoppingListItem(id: String, request: JsonElement) {
|
||||||
@@ -168,25 +169,19 @@ internal class MealieServiceKtor @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteShoppingListItem(id: String) {
|
override suspend fun deleteShoppingListItem(id: String) {
|
||||||
httpClient.delete {
|
httpClient.delete { endpoint("/api/households/shopping/items/$id") }
|
||||||
endpoint("/api/households/shopping/items/$id")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getFoods(perPage: Int): GetFoodsResponse {
|
override suspend fun getFoods(perPage: Int): GetFoodsResponse {
|
||||||
return httpClient.get {
|
return httpClient
|
||||||
endpoint("/api/foods") {
|
.get { endpoint("/api/foods") { parameters.append("perPage", perPage.toString()) } }
|
||||||
parameters.append("perPage", perPage.toString())
|
.body()
|
||||||
}
|
|
||||||
}.body()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUnits(perPage: Int): GetUnitsResponse {
|
override suspend fun getUnits(perPage: Int): GetUnitsResponse {
|
||||||
return httpClient.get {
|
return httpClient
|
||||||
endpoint("/api/units") {
|
.get { endpoint("/api/units") { parameters.append("perPage", perPage.toString()) } }
|
||||||
parameters.append("perPage", perPage.toString())
|
.body()
|
||||||
}
|
|
||||||
}.body()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createShoppingListItem(request: CreateShoppingListItemRequest) {
|
override suspend fun createShoppingListItem(request: CreateShoppingListItemRequest) {
|
||||||
@@ -206,9 +201,7 @@ internal class MealieServiceKtor @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteShoppingList(id: String) {
|
override suspend fun deleteShoppingList(id: String) {
|
||||||
httpClient.delete {
|
httpClient.delete { endpoint("/api/households/shopping/lists/$id") }
|
||||||
endpoint("/api/households/shopping/lists/$id")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateShoppingList(id: String, request: JsonElement) {
|
override suspend fun updateShoppingList(id: String, request: JsonElement) {
|
||||||
@@ -220,27 +213,25 @@ internal class MealieServiceKtor @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getShoppingListJson(id: String): JsonElement {
|
override suspend fun getShoppingListJson(id: String): JsonElement {
|
||||||
return httpClient.get {
|
return httpClient.get { endpoint("/api/households/shopping/lists/$id") }.body()
|
||||||
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(
|
private suspend fun HttpRequestBuilder.endpoint(
|
||||||
path: String,
|
path: String,
|
||||||
block: URLBuilder.() -> Unit = {},
|
block: URLBuilder.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val baseUrl = checkNotNull(serverUrlProvider.getUrl()) { "Server URL is not set" }
|
val baseUrl = checkNotNull(serverUrlProvider.getUrl()) { "Server URL is not set" }
|
||||||
endpoint(
|
endpoint(baseUrl = baseUrl, path = path, block = block)
|
||||||
baseUrl = baseUrl,
|
|
||||||
path = path,
|
|
||||||
block = block
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun HttpRequestBuilder.endpoint(
|
private fun HttpRequestBuilder.endpoint(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
path: String,
|
path: String,
|
||||||
block: URLBuilder.() -> Unit = {},
|
block: URLBuilder.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
url {
|
url {
|
||||||
takeFrom(baseUrl)
|
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]
|
[versions]
|
||||||
# https://maven.google.com/web/index.html?q=com.android.tools.build#com.android.tools.build:gradle
|
# 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
|
# https://github.com/JetBrains/kotlin/releases
|
||||||
kotlin = "2.0.10"
|
kotlin = "2.0.10"
|
||||||
# https://dagger.dev/hilt/gradle-setup
|
# 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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
|
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
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
Reference in New Issue
Block a user