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:
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
data class ParseRecipeURLInfo(
|
||||
val url: String,
|
||||
val includeTags: Boolean
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package gq.kirmanak.mealient.datasource.models
|
||||
|
||||
data class VersionInfo(
|
||||
val version: String,
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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 = "",
|
||||
)
|
||||
@@ -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 = "",
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -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?,
|
||||
)
|
||||
Reference in New Issue
Block a user