Implement shopping lists screen (#129)

* Initialize shopping lists feature

* Start shopping lists screen with Compose

* Add icon to shopping list name

* Add shopping lists to menu

* Set max size for the list

* Replace compose-adapter with accompanist

* Remove unused fields from shopping lists response

* Show list of shopping lists from BE

* Hide shopping lists if Mealie is 0.5.6

* Add shopping list item click listener

* Create material app theme for Compose

* Use shorter names

* Load shopping lists by pages and save to db

* Make page handling logic match recipes

* Add swipe to refresh to shopping lists

* Extract SwipeToRefresh Composable

* Make LazyPagingColumn generic

* Show refresh only when mediator is refreshing

* Do not refresh automatically

* Allow controlling Activity state from modules

* Implement navigating to shopping list screen

* Move Compose libraries setup to a plugin

* Implement loading full shopping list info

* Move Storage classes to database module

* Save shopping list items to DB

* Use separate names for separate ids

* Do only one DB version update

* Use unique names for all columns

* Display shopping list items

* Move OperationUiState to ui module

* Subscribe to shopping lists updates

* Indicate progress with progress bar

* Use strings from resources

* Format shopping list item quantities

* Hide unit/food/note/quantity if they are not set

* Implement updating shopping list item checked state

* Remove unnecessary null checks

* Disable checkbox when it is being updated

* Split shopping list screen into composables

* Show items immediately if they are saved

* Fix showing "list is empty" before the items

* Show Snackbar when error happens

* Reduce shopping list items paddings

* Remove shopping lists when URL is changed

* Add error/empty state handling to shopping lists

* Fix empty error state

* Fix tests compilation

* Add margin between text and button

* Add divider between checked and unchecked items

* Move divider to the item

* Refresh the shopping lists on authentication

* Use retry when necessary

* Remove excessive logging

* Fix pages bounds check

* Move FlowExtensionsTest

* Update Compose version

* Fix showing loading indicator for shopping lists

* Add Russian translation

* Fix SDK version lint check

* Rename parameter to match interface

* Add DB migration TODO

* Get rid of DB migrations

* Do not use pagination with shopping lists

* Cleanup after the pagination removal

* Load shopping list items

* Remove shopping lists storage

* Rethrow CancellationException in LoadingHelper

* Add pull-to-refresh on shopping list screen

* Extract LazyColumnWithLoadingState

* Split refresh errors and loading state

* Reuse LazyColumnWithLoadingState for shopping list items

* Remove paging-compose dependency

* Refresh shopping list items on authentication

* Disable missing translation lint check

* Update Compose and Kotlin versions

* Fix order of checked items

* Hide useless information from a shopping list item
This commit is contained in:
Kirill Kamakin
2023-07-03 15:07:19 +02:00
committed by GitHub
parent a40f9a78ea
commit 1e5e727e92
157 changed files with 3360 additions and 3715 deletions

View File

@@ -0,0 +1,23 @@
package gq.kirmanak.mealient.datasource.models
data class AddRecipeInfo(
val name: String,
val description: String,
val recipeYield: String,
val recipeIngredient: List<AddRecipeIngredientInfo>,
val recipeInstructions: List<AddRecipeInstructionInfo>,
val settings: AddRecipeSettingsInfo,
)
data class AddRecipeSettingsInfo(
val disableComments: Boolean,
val public: Boolean,
)
data class AddRecipeIngredientInfo(
val note: String,
)
data class AddRecipeInstructionInfo(
val text: String,
)

View File

@@ -0,0 +1,26 @@
package gq.kirmanak.mealient.datasource.models
data class FullRecipeInfo(
val remoteId: String,
val name: String,
val recipeYield: String,
val recipeIngredients: List<RecipeIngredientInfo>,
val recipeInstructions: List<RecipeInstructionInfo>,
val settings: RecipeSettingsInfo,
)
data class RecipeSettingsInfo(
val disableAmounts: Boolean,
)
data class RecipeIngredientInfo(
val note: String,
val quantity: Double?,
val unit: String?,
val food: String?,
val title: String?,
)
data class RecipeInstructionInfo(
val text: String,
)

View File

@@ -0,0 +1,28 @@
package gq.kirmanak.mealient.datasource.models
data class FullShoppingListInfo(
val id: String,
val name: String,
val items: List<ShoppingListItemInfo>,
)
data class ShoppingListItemInfo(
val shoppingListId: String,
val id: String,
val checked: Boolean,
val position: Int,
val isFood: Boolean,
val note: String,
val quantity: Double,
val unit: String,
val food: String,
val recipeReferences: List<ShoppingListItemRecipeReferenceInfo>,
)
data class ShoppingListItemRecipeReferenceInfo(
val recipeId: String,
val recipeQuantity: Double,
val id: String,
val shoppingListId: String,
val recipe: FullRecipeInfo,
)

View File

@@ -0,0 +1,6 @@
package gq.kirmanak.mealient.datasource.models
data class ParseRecipeURLInfo(
val url: String,
val includeTags: Boolean
)

View File

@@ -0,0 +1,14 @@
package gq.kirmanak.mealient.datasource.models
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
data class RecipeSummaryInfo(
val remoteId: String,
val name: String,
val slug: String,
val description: String = "",
val imageId: String,
val dateAdded: LocalDate,
val dateUpdated: LocalDateTime
)

View File

@@ -0,0 +1,14 @@
package gq.kirmanak.mealient.datasource.models
data class ShoppingListsInfo(
val page: Int,
val perPage: Int,
val totalPages: Int,
val totalItems: Int,
val items: List<ShoppingListInfo>,
)
data class ShoppingListInfo(
val name: String,
val id: String,
)

View File

@@ -0,0 +1,5 @@
package gq.kirmanak.mealient.datasource.models
data class VersionInfo(
val version: String,
)

View File

@@ -5,6 +5,8 @@ import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
@@ -54,5 +56,12 @@ interface MealieDataSourceV1 {
suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String)
suspend fun addFavoriteRecipe(userId: String, recipeSlug: String)
suspend fun deleteRecipe(slug: String)
suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponseV1
suspend fun getShoppingList(id: String): GetShoppingListResponseV1
suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean)
}

View File

@@ -9,12 +9,20 @@ import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import retrofit2.HttpException
import java.net.ConnectException
import java.net.SocketTimeoutException
@@ -134,5 +142,52 @@ class MealieDataSourceV1Impl @Inject constructor(
logMethod = { "deleteRecipe" },
logParameters = { "slug = $slug" }
)
}
override suspend fun getShoppingLists(
page: Int,
perPage: Int,
): GetShoppingListsResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getShoppingLists(page, perPage) },
logMethod = { "getShoppingLists" },
logParameters = { "page = $page, perPage = $perPage" }
)
override suspend fun getShoppingList(
id: String
): GetShoppingListResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getShoppingList(id) },
logMethod = { "getShoppingList" },
logParameters = { "id = $id" }
)
private suspend fun getShoppingListItem(
id: String,
): JsonElement = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getShoppingListItem(id) },
logMethod = { "getShoppingListItem" },
logParameters = { "id = $id" }
)
private suspend fun updateShoppingListItem(
id: String,
request: JsonElement,
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.updateShoppingListItem(id, request) },
logMethod = { "updateShoppingListItem" },
logParameters = { "id = $id, request = $request" }
)
override suspend fun updateIsShoppingListItemChecked(
id: String,
isChecked: Boolean
) {
// Has to be done in two steps because the API doesn't support updating the checked state
val item = getShoppingListItem(id)
val wasChecked = item.jsonObject.getValue("checked").jsonPrimitive.boolean
if (wasChecked == isChecked) return
val updatedItem = item.jsonObject.toMutableMap().apply {
put("checked", JsonPrimitive(isChecked))
}
updateShoppingListItem(id, JsonObject(updatedItem))
}
}

View File

@@ -1,6 +1,7 @@
package gq.kirmanak.mealient.datasource.v1
import gq.kirmanak.mealient.datasource.v1.models.*
import kotlinx.serialization.json.JsonElement
import retrofit2.http.*
interface MealieServiceV1 {
@@ -66,4 +67,26 @@ interface MealieServiceV1 {
suspend fun deleteRecipe(
@Path("slug") slug: String
)
@GET("/api/groups/shopping/lists")
suspend fun getShoppingLists(
@Query("page") page: Int,
@Query("perPage") perPage: Int,
): GetShoppingListsResponseV1
@GET("/api/groups/shopping/lists/{id}")
suspend fun getShoppingList(
@Path("id") id: String,
): GetShoppingListResponseV1
@GET("/api/groups/shopping/items/{id}")
suspend fun getShoppingListItem(
@Path("id") id: String,
): JsonElement
@PUT("/api/groups/shopping/items/{id}")
suspend fun updateShoppingListItem(
@Path("id") id: String,
@Body request: JsonElement,
)
}

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ data class GetRecipeResponseV1(
@SerialName("recipeYield") val recipeYield: String = "",
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponseV1> = emptyList(),
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponseV1> = emptyList(),
@SerialName("settings") val settings: GetRecipeSettingsResponseV1,
@SerialName("settings") val settings: GetRecipeSettingsResponseV1? = null,
)
@Serializable
@@ -27,16 +27,6 @@ data class GetRecipeIngredientResponseV1(
@SerialName("title") val title: String?,
)
@Serializable
data class GetRecipeIngredientFoodResponseV1(
@SerialName("name") val name: String = "",
)
@Serializable
data class GetRecipeIngredientUnitResponseV1(
@SerialName("name") val name: String = "",
)
@Serializable
data class GetRecipeInstructionResponseV1(
@SerialName("text") val text: String,

View File

@@ -0,0 +1,42 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetShoppingListResponseV1(
@SerialName("id") val id: String,
@SerialName("groupId") val groupId: String,
@SerialName("name") val name: String = "",
@SerialName("listItems") val listItems: List<GetShoppingListItemResponseV1> = emptyList(),
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceFullResponseV1>,
)
@Serializable
data class GetShoppingListItemResponseV1(
@SerialName("shoppingListId") val shoppingListId: String,
@SerialName("id") val id: String,
@SerialName("checked") val checked: Boolean = false,
@SerialName("position") val position: Int = 0,
@SerialName("isFood") val isFood: Boolean = false,
@SerialName("note") val note: String = "",
@SerialName("quantity") val quantity: Double = 0.0,
@SerialName("unit") val unit: GetRecipeIngredientUnitResponseV1? = null,
@SerialName("food") val food: GetRecipeIngredientFoodResponseV1? = null,
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceResponseV1> = emptyList(),
)
@Serializable
data class GetShoppingListItemRecipeReferenceResponseV1(
@SerialName("recipeId") val recipeId: String,
@SerialName("recipeQuantity") val recipeQuantity: Double = 0.0
)
@Serializable
data class GetShoppingListItemRecipeReferenceFullResponseV1(
@SerialName("id") val id: String,
@SerialName("shoppingListId") val shoppingListId: String,
@SerialName("recipeId") val recipeId: String,
@SerialName("recipeQuantity") val recipeQuantity: Double = 0.0,
@SerialName("recipe") val recipe: GetRecipeResponseV1,
)

View File

@@ -0,0 +1,13 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetShoppingListsResponseV1(
@SerialName("page") val page: Int,
@SerialName("per_page") val perPage: Int,
@SerialName("total") val total: Int,
@SerialName("total_pages") val totalPages: Int,
@SerialName("items") val items: List<GetShoppingListsSummaryResponseV1>,
)

View File

@@ -0,0 +1,10 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetShoppingListsSummaryResponseV1(
@SerialName("id") val id: String,
@SerialName("name") val name: String?,
)