Implement the dialog to add a new shopping list (#280)

* Add endpoint to create new shopping lists

* Initialize editing of lists names

* Implement adding new lists

* Fix invalid password for demo

* Use StateFlow to avoid lost state updates

* Refactor the list update to support empty lists

* Hide add new list button if there's a new list

* Scroll to the newly added list or item

* Replace deprecated Divider

* Move new field name input to dialog

* Display a modal dialog instead of bottom sheet

* Reduce unnecessary recompositions

* Do not hide button since it is overlapped by dialog

* Extract Composable for editable items

* Remove unused imports

* Add UI for removing and editing shopping lists

* Implement editing list name and removing lists

* Fix initial cursor state when editing name

* Add capitalization of list names

* Fix color of divider in dark mode
This commit is contained in:
Kirill Kamakin
2024-07-27 20:12:00 +02:00
committed by GitHub
parent f0098af3f6
commit 86e70e03d0
22 changed files with 1066 additions and 171 deletions

View File

@@ -4,6 +4,7 @@ import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
@@ -75,4 +76,10 @@ interface MealieDataSource {
suspend fun getUnits(): GetUnitsResponse
suspend fun addShoppingListItem(request: CreateShoppingListItemRequest)
suspend fun addShoppingList(request: CreateShoppingListRequest)
suspend fun deleteShoppingList(id: String)
suspend fun updateShoppingListName(id: String, name: String)
}

View File

@@ -4,6 +4,7 @@ import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipesResponse
@@ -61,4 +62,12 @@ internal interface MealieService {
suspend fun getUnits(perPage: Int): GetUnitsResponse
suspend fun createShoppingListItem(request: CreateShoppingListItemRequest)
suspend fun createShoppingList(request: CreateShoppingListRequest)
suspend fun deleteShoppingList(id: String)
suspend fun updateShoppingList(id: String, request: JsonElement)
suspend fun getShoppingListJson(id: String) : JsonElement
}

View File

@@ -8,6 +8,7 @@ import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
import gq.kirmanak.mealient.datasource.models.ErrorDetail
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
@@ -37,7 +38,7 @@ internal class MealieDataSourceImpl @Inject constructor(
) : MealieDataSource {
override suspend fun createRecipe(
recipe: CreateRecipeRequest
recipe: CreateRecipeRequest,
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createRecipe(recipe) },
logMethod = { "createRecipe" },
@@ -46,7 +47,7 @@ internal class MealieDataSourceImpl @Inject constructor(
override suspend fun updateRecipe(
slug: String,
recipe: UpdateRecipeRequest
recipe: UpdateRecipeRequest,
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.updateRecipe(recipe, slug) },
logMethod = { "updateRecipe" },
@@ -80,7 +81,7 @@ internal class MealieDataSourceImpl @Inject constructor(
override suspend fun requestRecipes(
page: Int,
perPage: Int
perPage: Int,
): List<GetRecipeSummaryResponse> = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getRecipeSummary(page, perPage) },
logMethod = { "requestRecipes" },
@@ -88,7 +89,7 @@ internal class MealieDataSourceImpl @Inject constructor(
).items
override suspend fun requestRecipeInfo(
slug: String
slug: String,
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getRecipe(slug) },
logMethod = { "requestRecipeInfo" },
@@ -96,7 +97,7 @@ internal class MealieDataSourceImpl @Inject constructor(
)
override suspend fun parseRecipeFromURL(
request: ParseRecipeURLRequest
request: ParseRecipeURLRequest,
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createRecipeFromURL(request) },
logMethod = { "parseRecipeFromURL" },
@@ -104,7 +105,7 @@ internal class MealieDataSourceImpl @Inject constructor(
)
override suspend fun createApiToken(
request: CreateApiTokenRequest
request: CreateApiTokenRequest,
): CreateApiTokenResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createApiToken(request) },
logMethod = { "createApiToken" },
@@ -120,7 +121,7 @@ internal class MealieDataSourceImpl @Inject constructor(
override suspend fun removeFavoriteRecipe(
userId: String,
recipeSlug: String
recipeSlug: String,
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.removeFavoriteRecipe(userId, recipeSlug) },
logMethod = { "removeFavoriteRecipe" },
@@ -129,7 +130,7 @@ internal class MealieDataSourceImpl @Inject constructor(
override suspend fun addFavoriteRecipe(
userId: String,
recipeSlug: String
recipeSlug: String,
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.addFavoriteRecipe(userId, recipeSlug) },
logMethod = { "addFavoriteRecipe" },
@@ -137,7 +138,7 @@ internal class MealieDataSourceImpl @Inject constructor(
)
override suspend fun deleteRecipe(
slug: String
slug: String,
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.deleteRecipe(slug) },
logMethod = { "deleteRecipe" },
@@ -154,7 +155,7 @@ internal class MealieDataSourceImpl @Inject constructor(
)
override suspend fun getShoppingList(
id: String
id: String,
): GetShoppingListResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getShoppingList(id) },
logMethod = { "getShoppingList" },
@@ -187,7 +188,7 @@ internal class MealieDataSourceImpl @Inject constructor(
)
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)
@@ -219,10 +220,55 @@ internal class MealieDataSourceImpl @Inject constructor(
}
override suspend fun addShoppingListItem(
request: CreateShoppingListItemRequest
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" }
)
private suspend fun updateShoppingList(
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" }
)
override suspend fun deleteShoppingList(
id: String,
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.deleteShoppingList(id) },
logMethod = { "deleteShoppingList" },
logParameters = { "id = $id" }
)
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)
updateShoppingList(id, updatedItem)
}
}

View File

@@ -6,6 +6,7 @@ import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipesResponse
@@ -196,6 +197,34 @@ internal class MealieServiceKtor @Inject constructor(
}
}
override suspend fun createShoppingList(request: CreateShoppingListRequest) {
httpClient.post {
endpoint("/api/groups/shopping/lists")
contentType(ContentType.Application.Json)
setBody(request)
}
}
override suspend fun deleteShoppingList(id: String) {
httpClient.delete {
endpoint("/api/groups/shopping/lists/$id")
}
}
override suspend fun updateShoppingList(id: String, request: JsonElement) {
httpClient.put {
endpoint("/api/groups/shopping/lists/$id")
contentType(ContentType.Application.Json)
setBody(request)
}
}
override suspend fun getShoppingListJson(id: String): JsonElement {
return httpClient.get {
endpoint("/api/groups/shopping/lists/$id")
}.body()
}
private suspend fun HttpRequestBuilder.endpoint(
path: String,
block: URLBuilder.() -> Unit = {},

View File

@@ -0,0 +1,9 @@
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateShoppingListRequest(
@SerialName("name") val name: String,
)