Implement adding and modifying shopping list items (#165)
* Add dismissed shopping list item preview * Implement editing of note and quantity * Add new editor row for food * Implement loading units and foods * Display dropdown for foods * Display dropdown for units * Implement updating food and units * Create secondary editor state constructor * Display "Add" button * Combine editing state to an object * Implement showing editor for new items * Implement saving new items * Log final screen state * Fix ordering of foods * Show keyboard when editing starts * Add bottom padding to the list * Show new items above checked
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.models
|
||||||
|
|
||||||
|
data class FoodInfo(
|
||||||
|
val name: String,
|
||||||
|
val id: String
|
||||||
|
)
|
||||||
@@ -14,8 +14,8 @@ data class ShoppingListItemInfo(
|
|||||||
val isFood: Boolean,
|
val isFood: Boolean,
|
||||||
val note: String,
|
val note: String,
|
||||||
val quantity: Double,
|
val quantity: Double,
|
||||||
val unit: String,
|
val unit: UnitInfo?,
|
||||||
val food: String,
|
val food: FoodInfo?,
|
||||||
val recipeReferences: List<ShoppingListItemRecipeReferenceInfo>,
|
val recipeReferences: List<ShoppingListItemRecipeReferenceInfo>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.models
|
||||||
|
|
||||||
|
data class NewShoppingListItemInfo(
|
||||||
|
val shoppingListId: String,
|
||||||
|
val isFood: Boolean,
|
||||||
|
val note: String,
|
||||||
|
val quantity: Double,
|
||||||
|
val unit: UnitInfo?,
|
||||||
|
val food: FoodInfo?,
|
||||||
|
val position: Int,
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.models
|
||||||
|
|
||||||
|
data class UnitInfo(
|
||||||
|
val name: String,
|
||||||
|
val id: String
|
||||||
|
)
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
package gq.kirmanak.mealient.datasource.v1
|
package gq.kirmanak.mealient.datasource.v1
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.CreateShoppingListItemRequestV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetFoodsResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetUnitsResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
|
||||||
@@ -63,7 +67,13 @@ interface MealieDataSourceV1 {
|
|||||||
|
|
||||||
suspend fun getShoppingList(id: String): GetShoppingListResponseV1
|
suspend fun getShoppingList(id: String): GetShoppingListResponseV1
|
||||||
|
|
||||||
suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean)
|
|
||||||
|
|
||||||
suspend fun deleteShoppingListItem(id: String)
|
suspend fun deleteShoppingListItem(id: String)
|
||||||
|
|
||||||
|
suspend fun updateShoppingListItem(item: ShoppingListItemInfo)
|
||||||
|
|
||||||
|
suspend fun getFoods(): GetFoodsResponseV1
|
||||||
|
|
||||||
|
suspend fun getUnits(): GetUnitsResponseV1
|
||||||
|
|
||||||
|
suspend fun addShoppingListItem(request: CreateShoppingListItemRequestV1)
|
||||||
}
|
}
|
||||||
@@ -3,14 +3,18 @@ package gq.kirmanak.mealient.datasource.v1
|
|||||||
import gq.kirmanak.mealient.datasource.NetworkError
|
import gq.kirmanak.mealient.datasource.NetworkError
|
||||||
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
|
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
|
||||||
import gq.kirmanak.mealient.datasource.decode
|
import gq.kirmanak.mealient.datasource.decode
|
||||||
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.CreateShoppingListItemRequestV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1
|
import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetFoodsResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetUnitsResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
|
||||||
@@ -20,9 +24,7 @@ import kotlinx.serialization.json.Json
|
|||||||
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.boolean
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import java.net.ConnectException
|
import java.net.ConnectException
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
@@ -175,20 +177,6 @@ class MealieDataSourceV1Impl @Inject constructor(
|
|||||||
logParameters = { "id = $id, request = $request" }
|
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun deleteShoppingListItem(
|
override suspend fun deleteShoppingListItem(
|
||||||
id: String,
|
id: String,
|
||||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
@@ -196,4 +184,44 @@ class MealieDataSourceV1Impl @Inject constructor(
|
|||||||
logMethod = { "deleteShoppingListItem" },
|
logMethod = { "deleteShoppingListItem" },
|
||||||
logParameters = { "id = $id" }
|
logParameters = { "id = $id" }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override suspend fun updateShoppingListItem(
|
||||||
|
item: ShoppingListItemInfo
|
||||||
|
) {
|
||||||
|
// Has to be done in two steps because we can't specify only the changed fields
|
||||||
|
val remoteItem = getShoppingListItem(item.id)
|
||||||
|
val updatedItem = remoteItem.jsonObject.toMutableMap().apply {
|
||||||
|
put("checked", JsonPrimitive(item.checked))
|
||||||
|
put("isFood", JsonPrimitive(item.isFood))
|
||||||
|
put("note", JsonPrimitive(item.note))
|
||||||
|
put("quantity", JsonPrimitive(item.quantity))
|
||||||
|
put("foodId", JsonPrimitive(item.food?.id))
|
||||||
|
put("unitId", JsonPrimitive(item.unit?.id))
|
||||||
|
remove("unit")
|
||||||
|
remove("food")
|
||||||
|
}
|
||||||
|
updateShoppingListItem(item.id, JsonObject(updatedItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getFoods(): GetFoodsResponseV1 {
|
||||||
|
return networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.getFoods(perPage = -1) },
|
||||||
|
logMethod = { "getFoods" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUnits(): GetUnitsResponseV1 {
|
||||||
|
return networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.getUnits(perPage = -1) },
|
||||||
|
logMethod = { "getUnits" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addShoppingListItem(
|
||||||
|
request: CreateShoppingListItemRequestV1
|
||||||
|
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.createShoppingListItem(request) },
|
||||||
|
logMethod = { "addShoppingListItem" },
|
||||||
|
logParameters = { "request = $request" }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,4 +94,19 @@ interface MealieServiceV1 {
|
|||||||
suspend fun deleteShoppingListItem(
|
suspend fun deleteShoppingListItem(
|
||||||
@Path("id") id: String,
|
@Path("id") id: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@GET("/api/foods")
|
||||||
|
suspend fun getFoods(
|
||||||
|
@Query("perPage") perPage: Int,
|
||||||
|
): GetFoodsResponseV1
|
||||||
|
|
||||||
|
@GET("/api/units")
|
||||||
|
suspend fun getUnits(
|
||||||
|
@Query("perPage") perPage: Int,
|
||||||
|
): GetUnitsResponseV1
|
||||||
|
|
||||||
|
@POST("/api/groups/shopping/items")
|
||||||
|
suspend fun createShoppingListItem(
|
||||||
|
@Body request: CreateShoppingListItemRequestV1,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.v1.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateShoppingListItemRequestV1(
|
||||||
|
@SerialName("shopping_list_id") val shoppingListId: String,
|
||||||
|
@SerialName("checked") val checked: Boolean,
|
||||||
|
@SerialName("position") val position: Int?,
|
||||||
|
@SerialName("is_food") val isFood: Boolean,
|
||||||
|
@SerialName("note") val note: String,
|
||||||
|
@SerialName("quantity") val quantity: Double,
|
||||||
|
@SerialName("food_id") val foodId: String?,
|
||||||
|
@SerialName("unit_id") val unitId: String?,
|
||||||
|
)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.v1.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GetFoodsResponseV1(
|
||||||
|
@SerialName("items") val items: List<GetFoodResponseV1>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GetFoodResponseV1(
|
||||||
|
@SerialName("name") val name: String,
|
||||||
|
@SerialName("id") val id: String,
|
||||||
|
)
|
||||||
@@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class GetRecipeIngredientFoodResponseV1(
|
data class GetRecipeIngredientFoodResponseV1(
|
||||||
@SerialName("name") val name: String = "",
|
@SerialName("name") val name: String = "",
|
||||||
|
@SerialName("id") val id: String = "",
|
||||||
)
|
)
|
||||||
@@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class GetRecipeIngredientUnitResponseV1(
|
data class GetRecipeIngredientUnitResponseV1(
|
||||||
@SerialName("name") val name: String = "",
|
@SerialName("name") val name: String = "",
|
||||||
|
@SerialName("id") val id: String = "",
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.v1.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GetUnitsResponseV1(
|
||||||
|
@SerialName("items") val items: List<GetUnitResponseV1>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GetUnitResponseV1(
|
||||||
|
@SerialName("name") val name: String,
|
||||||
|
@SerialName("id") val id: String
|
||||||
|
)
|
||||||
@@ -22,6 +22,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.android.material.material)
|
implementation(libs.android.material.material)
|
||||||
implementation(libs.androidx.compose.material)
|
implementation(libs.androidx.compose.material)
|
||||||
|
implementation(libs.androidx.compose.materialIconsExtended)
|
||||||
|
|
||||||
implementation(libs.google.dagger.hiltAndroid)
|
implementation(libs.google.dagger.hiltAndroid)
|
||||||
kapt(libs.google.dagger.hiltCompiler)
|
kapt(libs.google.dagger.hiltCompiler)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.network
|
package gq.kirmanak.mealient.shopping_lists.network
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||||
|
|
||||||
interface ShoppingListsDataSource {
|
interface ShoppingListsDataSource {
|
||||||
|
|
||||||
@@ -9,7 +13,13 @@ interface ShoppingListsDataSource {
|
|||||||
|
|
||||||
suspend fun getShoppingList(id: String): FullShoppingListInfo
|
suspend fun getShoppingList(id: String): FullShoppingListInfo
|
||||||
|
|
||||||
suspend fun updateIsShoppingListItemChecked(id: String, checked: Boolean)
|
|
||||||
|
|
||||||
suspend fun deleteShoppingListItem(id: String)
|
suspend fun deleteShoppingListItem(id: String)
|
||||||
|
|
||||||
|
suspend fun updateShoppingListItem(item: ShoppingListItemInfo)
|
||||||
|
|
||||||
|
suspend fun getFoods(): List<FoodInfo>
|
||||||
|
|
||||||
|
suspend fun getUnits(): List<UnitInfo>
|
||||||
|
|
||||||
|
suspend fun addShoppingListItem(item: NewShoppingListItemInfo)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.network
|
package gq.kirmanak.mealient.shopping_lists.network
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||||
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
||||||
import gq.kirmanak.mealient.model_mapper.ModelMapper
|
import gq.kirmanak.mealient.model_mapper.ModelMapper
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -20,14 +24,20 @@ class ShoppingListsDataSourceImpl @Inject constructor(
|
|||||||
id: String
|
id: String
|
||||||
): FullShoppingListInfo = modelMapper.toFullShoppingListInfo(v1Source.getShoppingList(id))
|
): FullShoppingListInfo = modelMapper.toFullShoppingListInfo(v1Source.getShoppingList(id))
|
||||||
|
|
||||||
override suspend fun updateIsShoppingListItemChecked(
|
|
||||||
id: String,
|
|
||||||
checked: Boolean,
|
|
||||||
) = v1Source.updateIsShoppingListItemChecked(id, checked)
|
|
||||||
|
|
||||||
override suspend fun deleteShoppingListItem(
|
override suspend fun deleteShoppingListItem(
|
||||||
id: String
|
id: String
|
||||||
) = v1Source.deleteShoppingListItem(id)
|
) = v1Source.deleteShoppingListItem(id)
|
||||||
|
|
||||||
|
override suspend fun updateShoppingListItem(
|
||||||
|
item: ShoppingListItemInfo
|
||||||
|
) = v1Source.updateShoppingListItem(item)
|
||||||
|
|
||||||
|
override suspend fun getFoods(): List<FoodInfo> = modelMapper.toFoodInfo(v1Source.getFoods())
|
||||||
|
|
||||||
|
override suspend fun getUnits(): List<UnitInfo> = modelMapper.toUnitInfo(v1Source.getUnits())
|
||||||
|
|
||||||
|
override suspend fun addShoppingListItem(
|
||||||
|
item: NewShoppingListItemInfo
|
||||||
|
) = v1Source.addShoppingListItem(modelMapper.toV1CreateRequest(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.repo
|
package gq.kirmanak.mealient.shopping_lists.repo
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||||
|
|
||||||
interface ShoppingListsRepo {
|
interface ShoppingListsRepo {
|
||||||
|
|
||||||
suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean)
|
|
||||||
|
|
||||||
suspend fun getShoppingLists(): List<ShoppingListInfo>
|
suspend fun getShoppingLists(): List<ShoppingListInfo>
|
||||||
|
|
||||||
suspend fun getShoppingList(id: String): FullShoppingListInfo
|
suspend fun getShoppingList(id: String): FullShoppingListInfo
|
||||||
|
|
||||||
suspend fun deleteShoppingListItem(id: String)
|
suspend fun deleteShoppingListItem(id: String)
|
||||||
|
|
||||||
|
suspend fun updateShoppingListItem(item: ShoppingListItemInfo)
|
||||||
|
|
||||||
|
suspend fun getFoods(): List<FoodInfo>
|
||||||
|
|
||||||
|
suspend fun getUnits(): List<UnitInfo>
|
||||||
|
|
||||||
|
suspend fun addShoppingListItem(item: NewShoppingListItemInfo)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.repo
|
package gq.kirmanak.mealient.shopping_lists.repo
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.shopping_lists.network.ShoppingListsDataSource
|
import gq.kirmanak.mealient.shopping_lists.network.ShoppingListsDataSource
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -11,11 +15,6 @@ class ShoppingListsRepoImpl @Inject constructor(
|
|||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
) : ShoppingListsRepo {
|
) : ShoppingListsRepo {
|
||||||
|
|
||||||
override suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean) {
|
|
||||||
logger.v { "updateIsShoppingListItemChecked() called with: id = $id, isChecked = $isChecked" }
|
|
||||||
dataSource.updateIsShoppingListItemChecked(id, isChecked)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getShoppingLists(): List<ShoppingListInfo> {
|
override suspend fun getShoppingLists(): List<ShoppingListInfo> {
|
||||||
logger.v { "getShoppingLists() called" }
|
logger.v { "getShoppingLists() called" }
|
||||||
return dataSource.getAllShoppingLists()
|
return dataSource.getAllShoppingLists()
|
||||||
@@ -30,4 +29,24 @@ class ShoppingListsRepoImpl @Inject constructor(
|
|||||||
logger.v { "deleteShoppingListItem() called with: id = $id" }
|
logger.v { "deleteShoppingListItem() called with: id = $id" }
|
||||||
dataSource.deleteShoppingListItem(id)
|
dataSource.deleteShoppingListItem(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateShoppingListItem(item: ShoppingListItemInfo) {
|
||||||
|
logger.v { "updateShoppingListItem() called with: item = $item" }
|
||||||
|
dataSource.updateShoppingListItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getFoods(): List<FoodInfo> {
|
||||||
|
logger.v { "getFoods() called" }
|
||||||
|
return dataSource.getFoods()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUnits(): List<UnitInfo> {
|
||||||
|
logger.v { "getUnits() called" }
|
||||||
|
return dataSource.getUnits()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addShoppingListItem(item: NewShoppingListItemInfo) {
|
||||||
|
logger.v { "addShoppingListItem() called with: item = $item" }
|
||||||
|
dataSource.addShoppingListItem(item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package gq.kirmanak.mealient.shopping_lists.ui
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||||
|
|
||||||
|
data class ShoppingListData(
|
||||||
|
val foods: List<FoodInfo>,
|
||||||
|
val units: List<UnitInfo>,
|
||||||
|
val shoppingList: FullShoppingListInfo,
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package gq.kirmanak.mealient.shopping_lists.ui
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
|
|
||||||
|
data class ShoppingListEditingState(
|
||||||
|
val deletedItemIds: Set<String> = emptySet(),
|
||||||
|
val editingItemIds: Set<String> = emptySet(),
|
||||||
|
val modifiedItems: Map<String, ShoppingListItemInfo> = emptyMap(),
|
||||||
|
val newItems: List<ShoppingListItemState.NewItem> = emptyList(),
|
||||||
|
)
|
||||||
@@ -11,45 +11,69 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.NoMeals
|
||||||
|
import androidx.compose.material.icons.filled.Restaurant
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.DismissDirection
|
import androidx.compose.material3.DismissState
|
||||||
import androidx.compose.material3.DismissValue
|
import androidx.compose.material3.DismissValue
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.SwipeToDismiss
|
import androidx.compose.material3.SwipeToDismiss
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberDismissState
|
import androidx.compose.material3.rememberDismissState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import gq.kirmanak.mealient.AppTheme
|
import gq.kirmanak.mealient.AppTheme
|
||||||
import gq.kirmanak.mealient.Dimens
|
import gq.kirmanak.mealient.Dimens
|
||||||
|
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.FullRecipeInfo
|
import gq.kirmanak.mealient.datasource.models.FullRecipeInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo
|
import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo
|
import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.RecipeSettingsInfo
|
import gq.kirmanak.mealient.datasource.models.RecipeSettingsInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemRecipeReferenceInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemRecipeReferenceInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||||
import gq.kirmanak.mealient.shopping_list.R
|
import gq.kirmanak.mealient.shopping_list.R
|
||||||
import gq.kirmanak.mealient.shopping_lists.ui.composables.LazyColumnWithLoadingState
|
import gq.kirmanak.mealient.shopping_lists.ui.composables.LazyColumnWithLoadingState
|
||||||
import gq.kirmanak.mealient.shopping_lists.util.data
|
import gq.kirmanak.mealient.shopping_lists.util.data
|
||||||
import gq.kirmanak.mealient.shopping_lists.util.map
|
import gq.kirmanak.mealient.shopping_lists.util.map
|
||||||
|
import kotlinx.coroutines.android.awaitFrame
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
data class ShoppingListNavArgs(
|
data class ShoppingListNavArgs(
|
||||||
val shoppingListId: String,
|
val shoppingListId: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Destination(
|
@Destination(
|
||||||
navArgsDelegate = ShoppingListNavArgs::class,
|
navArgsDelegate = ShoppingListNavArgs::class,
|
||||||
)
|
)
|
||||||
@@ -65,54 +89,415 @@ internal fun ShoppingListScreen(
|
|||||||
|
|
||||||
LazyColumnWithLoadingState(
|
LazyColumnWithLoadingState(
|
||||||
loadingState = loadingState.map { it.items },
|
loadingState = loadingState.map { it.items },
|
||||||
contentPadding = PaddingValues(Dimens.Medium),
|
contentPadding = PaddingValues(
|
||||||
|
start = Dimens.Medium,
|
||||||
|
end = Dimens.Medium,
|
||||||
|
top = Dimens.Medium,
|
||||||
|
bottom = Dimens.Large * 4,
|
||||||
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(Dimens.Medium),
|
verticalArrangement = Arrangement.spacedBy(Dimens.Medium),
|
||||||
defaultEmptyListError = defaultEmptyListError,
|
defaultEmptyListError = defaultEmptyListError,
|
||||||
errorToShowInSnackbar = shoppingListViewModel.errorToShowInSnackbar,
|
errorToShowInSnackbar = shoppingListViewModel.errorToShowInSnackbar,
|
||||||
onRefresh = shoppingListViewModel::refreshShoppingList,
|
onRefresh = shoppingListViewModel::refreshShoppingList,
|
||||||
onSnackbarShown = shoppingListViewModel::onSnackbarShown
|
onSnackbarShown = shoppingListViewModel::onSnackbarShown,
|
||||||
|
floatingActionButton = {
|
||||||
|
FloatingActionButton(onClick = shoppingListViewModel::onAddItemClicked) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_list_screen_add_icon_content_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
) { items ->
|
) { items ->
|
||||||
val firstCheckedItemIndex = items.indexOfFirst { it.checked }
|
val firstCheckedItemIndex = items.indexOfFirst { it.checked }
|
||||||
|
|
||||||
itemsIndexed(items, { _, item -> item.id }) { index, item ->
|
itemsIndexed(items, { _, item -> item.id }) { index, itemState ->
|
||||||
ShoppingListItem(
|
if (itemState is ShoppingListItemState.ExistingItem) {
|
||||||
shoppingListItem = item,
|
if (itemState.isEditing) {
|
||||||
showDivider = index == firstCheckedItemIndex && index != 0,
|
val state = remember {
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
ShoppingListItemEditorState(
|
||||||
onCheckedChange = { isChecked ->
|
state = itemState,
|
||||||
shoppingListViewModel.onItemCheckedChange(item, isChecked)
|
foods = loadingState.data?.foods.orEmpty(),
|
||||||
},
|
units = loadingState.data?.units.orEmpty(),
|
||||||
onDismissed = {
|
)
|
||||||
shoppingListViewModel.deleteShoppingListItem(item)
|
}
|
||||||
|
ShoppingListItemEditor(
|
||||||
|
state = state,
|
||||||
|
onEditCancelled = { shoppingListViewModel.onEditCancel(itemState) },
|
||||||
|
onEditConfirmed = { shoppingListViewModel.onEditConfirm(itemState, state) }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ShoppingListItem(
|
||||||
|
itemState = itemState,
|
||||||
|
showDivider = index == firstCheckedItemIndex && index != 0,
|
||||||
|
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||||
|
onCheckedChange = {
|
||||||
|
shoppingListViewModel.onItemCheckedChange(itemState, it)
|
||||||
|
},
|
||||||
|
onDismissed = { shoppingListViewModel.deleteShoppingListItem(itemState) },
|
||||||
|
onEditStart = { shoppingListViewModel.onEditStart(itemState) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
} else if (itemState is ShoppingListItemState.NewItem) {
|
||||||
|
ShoppingListItemEditor(
|
||||||
|
state = itemState.item,
|
||||||
|
onEditCancelled = { shoppingListViewModel.onAddCancel(itemState) },
|
||||||
|
onEditConfirmed = { shoppingListViewModel.onAddConfirm(itemState) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShoppingListItemEditor(
|
||||||
|
state: ShoppingListItemEditorState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onEditCancelled: () -> Unit = {},
|
||||||
|
onEditConfirmed: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimens.Small),
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
) {
|
||||||
|
ShoppingListItemEditorFirstRow(
|
||||||
|
state = state
|
||||||
|
)
|
||||||
|
if (state.isFood) {
|
||||||
|
ShoppingListItemEditorFoodRow(state = state)
|
||||||
|
}
|
||||||
|
ShoppingListItemEditorButtonRow(
|
||||||
|
state = state,
|
||||||
|
onEditCancelled = onEditCancelled,
|
||||||
|
onEditConfirmed = onEditConfirmed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShoppingListItemEditorFirstRow(
|
||||||
|
state: ShoppingListItemEditorState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.Small),
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.quantity,
|
||||||
|
onValueChange = { newValue ->
|
||||||
|
val input = newValue.trim()
|
||||||
|
.let {
|
||||||
|
if (state.quantity == "0") {
|
||||||
|
it.removeSuffix("0").removePrefix("0")
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ifEmpty { "0" }
|
||||||
|
if (input.toDoubleOrNull() != null) {
|
||||||
|
state.quantity = input
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.shopping_list_screen_editor_quantity_label),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Number,
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.note,
|
||||||
|
onValueChange = { state.note = it },
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.shopping_list_screen_editor_note_label),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(3f, true)
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(focusRequester) {
|
||||||
|
awaitFrame()
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShoppingListItemEditorButtonRow(
|
||||||
|
state: ShoppingListItemEditorState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onEditCancelled: () -> Unit = {},
|
||||||
|
onEditConfirmed: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.Small)
|
||||||
|
) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
state.isFood = !state.isFood
|
||||||
|
}) {
|
||||||
|
val stringId = if (state.isFood) {
|
||||||
|
R.string.shopping_list_screen_editor_not_food_button
|
||||||
|
} else {
|
||||||
|
R.string.shopping_list_screen_editor_food_button
|
||||||
|
}
|
||||||
|
val icon = if (state.isFood) {
|
||||||
|
Icons.Default.NoMeals
|
||||||
|
} else {
|
||||||
|
Icons.Default.Restaurant
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = stringResource(id = stringId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = onEditCancelled) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_list_screen_editor_cancel_button)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = onEditConfirmed) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_list_screen_editor_save_button)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun ShoppingListItemEditorFoodRow(
|
||||||
|
state: ShoppingListItemEditorState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.Small),
|
||||||
|
) {
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = state.foodsExpanded,
|
||||||
|
onExpandedChange = { state.foodsExpanded = it },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.food?.name.orEmpty(),
|
||||||
|
onValueChange = { },
|
||||||
|
modifier = Modifier.menuAnchor(),
|
||||||
|
readOnly = true,
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.shopping_list_screen_editor_food_label),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = state.foodsExpanded)
|
||||||
|
},
|
||||||
|
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = state.foodsExpanded,
|
||||||
|
onDismissRequest = { state.foodsExpanded = false }
|
||||||
|
) {
|
||||||
|
state.foods.forEach {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(text = it.name, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
state.food = it
|
||||||
|
state.foodsExpanded = false
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
if (it == state.food) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_list_screen_editor_checked_unit_content_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = state.unitsExpanded,
|
||||||
|
onExpandedChange = { state.unitsExpanded = it },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.unit?.name.orEmpty(),
|
||||||
|
onValueChange = { },
|
||||||
|
modifier = Modifier.menuAnchor(),
|
||||||
|
readOnly = true,
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.shopping_list_screen_editor_unit_label),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = state.unitsExpanded)
|
||||||
|
},
|
||||||
|
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = state.unitsExpanded,
|
||||||
|
onDismissRequest = { state.unitsExpanded = false }
|
||||||
|
) {
|
||||||
|
state.units.forEach {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(text = it.name, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
state.unit = it
|
||||||
|
state.unitsExpanded = false
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
if (it == state.unit) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_list_screen_editor_checked_unit_content_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShoppingListItemEditorState(
|
||||||
|
val foods: List<FoodInfo>,
|
||||||
|
val units: List<UnitInfo>,
|
||||||
|
val position: Int,
|
||||||
|
val listId: String,
|
||||||
|
note: String = "",
|
||||||
|
quantity: String = "1.0",
|
||||||
|
isFood: Boolean = false,
|
||||||
|
food: FoodInfo? = null,
|
||||||
|
unit: UnitInfo? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
state: ShoppingListItemState.ExistingItem,
|
||||||
|
foods: List<FoodInfo>,
|
||||||
|
units: List<UnitInfo>,
|
||||||
|
) : this(
|
||||||
|
foods = foods,
|
||||||
|
units = units,
|
||||||
|
position = state.item.position,
|
||||||
|
listId = state.item.shoppingListId,
|
||||||
|
note = state.item.note,
|
||||||
|
quantity = state.item.quantity.toString(),
|
||||||
|
isFood = state.item.isFood,
|
||||||
|
food = state.item.food,
|
||||||
|
unit = state.item.unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
var note: String by mutableStateOf(note)
|
||||||
|
|
||||||
|
var quantity: String by mutableStateOf(quantity)
|
||||||
|
|
||||||
|
var isFood: Boolean by mutableStateOf(isFood)
|
||||||
|
|
||||||
|
var food: FoodInfo? by mutableStateOf(food)
|
||||||
|
|
||||||
|
var unit: UnitInfo? by mutableStateOf(unit)
|
||||||
|
|
||||||
|
var foodsExpanded: Boolean by mutableStateOf(false)
|
||||||
|
|
||||||
|
var unitsExpanded: Boolean by mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ShoppingListItemEditorPreview() {
|
||||||
|
AppTheme {
|
||||||
|
ShoppingListItemEditor(
|
||||||
|
state = ShoppingListItemEditorState(
|
||||||
|
state = ShoppingListItemState.ExistingItem(PreviewData.milk),
|
||||||
|
foods = emptyList(),
|
||||||
|
units = emptyList(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ShoppingListItemEditorNonFoodPreview() {
|
||||||
|
AppTheme {
|
||||||
|
ShoppingListItemEditor(
|
||||||
|
state = ShoppingListItemEditorState(
|
||||||
|
state = ShoppingListItemState.ExistingItem(PreviewData.blackTeaBags),
|
||||||
|
foods = emptyList(),
|
||||||
|
units = emptyList(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ShoppingListItem(
|
fun ShoppingListItem(
|
||||||
shoppingListItem: ShoppingListItemInfo,
|
itemState: ShoppingListItemState.ExistingItem,
|
||||||
showDivider: Boolean,
|
showDivider: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onCheckedChange: (Boolean) -> Unit = {},
|
onCheckedChange: (Boolean) -> Unit = {},
|
||||||
onDismissed: () -> Unit = {},
|
onDismissed: () -> Unit = {},
|
||||||
) {
|
onEditStart: () -> Unit = {},
|
||||||
val dismissState = rememberDismissState(
|
dismissState: DismissState = rememberDismissState(
|
||||||
confirmValueChange = {
|
confirmValueChange = {
|
||||||
if (it == DismissValue.DismissedToStart) {
|
when (it) {
|
||||||
onDismissed()
|
DismissValue.DismissedToStart -> onDismissed()
|
||||||
|
DismissValue.DismissedToEnd -> onEditStart()
|
||||||
|
DismissValue.Default -> Unit
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
) {
|
||||||
|
val shoppingListItem = itemState.item
|
||||||
SwipeToDismiss(
|
SwipeToDismiss(
|
||||||
state = dismissState,
|
state = dismissState,
|
||||||
background = {
|
background = {
|
||||||
if (dismissState.targetValue == DismissValue.DismissedToStart) {
|
if (dismissState.targetValue == DismissValue.DismissedToStart) {
|
||||||
val color by animateColorAsState(MaterialTheme.colorScheme.error)
|
val color by animateColorAsState(MaterialTheme.colorScheme.error)
|
||||||
val iconColor by animateColorAsState(MaterialTheme.colorScheme.onSurface)
|
val iconColor by animateColorAsState(MaterialTheme.colorScheme.onError)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -127,6 +512,23 @@ fun ShoppingListItem(
|
|||||||
.padding(end = Dimens.Small)
|
.padding(end = Dimens.Small)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (dismissState.targetValue == DismissValue.DismissedToEnd) {
|
||||||
|
val color by animateColorAsState(MaterialTheme.colorScheme.primary)
|
||||||
|
val iconColor by animateColorAsState(MaterialTheme.colorScheme.onPrimary)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(color)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Edit,
|
||||||
|
contentDescription = stringResource(R.string.shopping_list_screen_edit_icon_content_description),
|
||||||
|
tint = iconColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.padding(start = Dimens.Small)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissContent = {
|
dismissContent = {
|
||||||
@@ -143,7 +545,7 @@ fun ShoppingListItem(
|
|||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
) {
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = shoppingListItem.checked,
|
checked = itemState.item.checked,
|
||||||
onCheckedChange = onCheckedChange,
|
onCheckedChange = onCheckedChange,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -154,8 +556,8 @@ fun ShoppingListItem(
|
|||||||
?.let { DecimalFormat.getInstance().format(it) }
|
?.let { DecimalFormat.getInstance().format(it) }
|
||||||
val text = listOfNotNull(
|
val text = listOfNotNull(
|
||||||
quantity,
|
quantity,
|
||||||
shoppingListItem.unit.takeIf { isFood },
|
shoppingListItem.unit.takeIf { isFood }?.name,
|
||||||
shoppingListItem.food.takeIf { isFood },
|
shoppingListItem.food.takeIf { isFood }?.name,
|
||||||
shoppingListItem.note,
|
shoppingListItem.note,
|
||||||
).filter { it.isNotBlank() }.joinToString(" ")
|
).filter { it.isNotBlank() }.joinToString(" ")
|
||||||
|
|
||||||
@@ -164,23 +566,60 @@ fun ShoppingListItem(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
directions = setOf(DismissDirection.EndToStart),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun PreviewShoppingListItemChecked() {
|
fun PreviewShoppingListItemChecked() {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
ShoppingListItem(shoppingListItem = PreviewData.milk, false)
|
ShoppingListItem(
|
||||||
|
itemState = ShoppingListItemState.ExistingItem(PreviewData.milk),
|
||||||
|
showDivider = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun PreviewShoppingListItemUnchecked() {
|
fun PreviewShoppingListItemUnchecked() {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
ShoppingListItem(shoppingListItem = PreviewData.blackTeaBags, false)
|
ShoppingListItem(
|
||||||
|
itemState = ShoppingListItemState.ExistingItem(PreviewData.blackTeaBags),
|
||||||
|
showDivider = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun PreviewShoppingListItemDismissed() {
|
||||||
|
AppTheme {
|
||||||
|
ShoppingListItem(
|
||||||
|
itemState = ShoppingListItemState.ExistingItem(PreviewData.blackTeaBags),
|
||||||
|
showDivider = false,
|
||||||
|
dismissState = rememberDismissState(
|
||||||
|
initialValue = DismissValue.DismissedToStart,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun PreviewShoppingListItemEditing() {
|
||||||
|
AppTheme {
|
||||||
|
ShoppingListItem(
|
||||||
|
itemState = ShoppingListItemState.ExistingItem(PreviewData.blackTeaBags),
|
||||||
|
showDivider = false,
|
||||||
|
dismissState = rememberDismissState(
|
||||||
|
initialValue = DismissValue.DismissedToEnd,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,8 +665,8 @@ private object PreviewData {
|
|||||||
isFood = false,
|
isFood = false,
|
||||||
note = "Black tea bags",
|
note = "Black tea bags",
|
||||||
quantity = 30.0,
|
quantity = 30.0,
|
||||||
unit = "",
|
unit = null,
|
||||||
food = "",
|
food = null,
|
||||||
recipeReferences = listOf(
|
recipeReferences = listOf(
|
||||||
ShoppingListItemRecipeReferenceInfo(
|
ShoppingListItemRecipeReferenceInfo(
|
||||||
shoppingListId = "1",
|
shoppingListId = "1",
|
||||||
@@ -243,12 +682,12 @@ private object PreviewData {
|
|||||||
id = "2",
|
id = "2",
|
||||||
shoppingListId = "1",
|
shoppingListId = "1",
|
||||||
checked = true,
|
checked = true,
|
||||||
position = 1,
|
position = 0,
|
||||||
isFood = true,
|
isFood = true,
|
||||||
note = "Cold",
|
note = "Cold",
|
||||||
quantity = 500.0,
|
quantity = 500.0,
|
||||||
unit = "ml",
|
unit = UnitInfo("ml", ""),
|
||||||
food = "Milk",
|
food = FoodInfo("Milk", ""),
|
||||||
recipeReferences = listOf(
|
recipeReferences = listOf(
|
||||||
ShoppingListItemRecipeReferenceInfo(
|
ShoppingListItemRecipeReferenceInfo(
|
||||||
shoppingListId = "1",
|
shoppingListId = "1",
|
||||||
|
|||||||
@@ -1,8 +1,45 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.ui
|
package gq.kirmanak.mealient.shopping_lists.ui
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
internal data class ShoppingListScreenState(
|
internal data class ShoppingListScreenState(
|
||||||
val name: String,
|
val name: String,
|
||||||
val items: List<ShoppingListItemInfo>,
|
val listId: String,
|
||||||
|
val items: List<ShoppingListItemState>,
|
||||||
|
val foods: List<FoodInfo>,
|
||||||
|
val units: List<UnitInfo>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sealed class ShoppingListItemState {
|
||||||
|
|
||||||
|
data class ExistingItem(
|
||||||
|
val item: ShoppingListItemInfo,
|
||||||
|
val isEditing: Boolean = false,
|
||||||
|
) : ShoppingListItemState()
|
||||||
|
|
||||||
|
data class NewItem(
|
||||||
|
val item: ShoppingListItemEditorState,
|
||||||
|
val id: String = UUID.randomUUID().toString(),
|
||||||
|
) : ShoppingListItemState()
|
||||||
|
}
|
||||||
|
|
||||||
|
val ShoppingListItemState.id: String
|
||||||
|
get() = when (this) {
|
||||||
|
is ShoppingListItemState.ExistingItem -> item.id
|
||||||
|
is ShoppingListItemState.NewItem -> id
|
||||||
|
}
|
||||||
|
|
||||||
|
val ShoppingListItemState.checked: Boolean
|
||||||
|
get() = when (this) {
|
||||||
|
is ShoppingListItemState.ExistingItem -> item.checked
|
||||||
|
is ShoppingListItemState.NewItem -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
val ShoppingListItemState.position: Int
|
||||||
|
get() = when (this) {
|
||||||
|
is ShoppingListItemState.ExistingItem -> item.position
|
||||||
|
is ShoppingListItemState.NewItem -> item.position
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import gq.kirmanak.mealient.architecture.valueUpdatesOnly
|
import gq.kirmanak.mealient.architecture.valueUpdatesOnly
|
||||||
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
@@ -18,7 +18,10 @@ import gq.kirmanak.mealient.shopping_lists.ui.destinations.ShoppingListScreenDes
|
|||||||
import gq.kirmanak.mealient.shopping_lists.util.LoadingHelperFactory
|
import gq.kirmanak.mealient.shopping_lists.util.LoadingHelperFactory
|
||||||
import gq.kirmanak.mealient.shopping_lists.util.LoadingState
|
import gq.kirmanak.mealient.shopping_lists.util.LoadingState
|
||||||
import gq.kirmanak.mealient.shopping_lists.util.LoadingStateNoData
|
import gq.kirmanak.mealient.shopping_lists.util.LoadingStateNoData
|
||||||
|
import gq.kirmanak.mealient.shopping_lists.util.data
|
||||||
import gq.kirmanak.mealient.shopping_lists.util.map
|
import gq.kirmanak.mealient.shopping_lists.util.map
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -39,18 +42,15 @@ internal class ShoppingListViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val args: ShoppingListNavArgs = ShoppingListScreenDestination.argsFrom(savedStateHandle)
|
private val args: ShoppingListNavArgs = ShoppingListScreenDestination.argsFrom(savedStateHandle)
|
||||||
|
|
||||||
private val checkedOverride = MutableStateFlow<MutableMap<String, Boolean>>(mutableMapOf())
|
private val editingStateFlow = MutableStateFlow(ShoppingListEditingState())
|
||||||
|
|
||||||
private val deletedItemIds = MutableStateFlow<Set<String>>(mutableSetOf())
|
|
||||||
|
|
||||||
private val loadingHelper = loadingHelperFactory.create(viewModelScope) {
|
private val loadingHelper = loadingHelperFactory.create(viewModelScope) {
|
||||||
shoppingListsRepo.getShoppingList(args.shoppingListId)
|
loadShoppingListData()
|
||||||
}
|
}
|
||||||
|
|
||||||
val loadingState: StateFlow<LoadingState<ShoppingListScreenState>> = combine(
|
val loadingState: StateFlow<LoadingState<ShoppingListScreenState>> = combine(
|
||||||
loadingHelper.loadingState,
|
loadingHelper.loadingState,
|
||||||
checkedOverride,
|
editingStateFlow,
|
||||||
deletedItemIds,
|
|
||||||
::buildLoadingState,
|
::buildLoadingState,
|
||||||
).stateIn(viewModelScope, SharingStarted.Eagerly, LoadingStateNoData.InitialLoad)
|
).stateIn(viewModelScope, SharingStarted.Eagerly, LoadingStateNoData.InitialLoad)
|
||||||
|
|
||||||
@@ -79,46 +79,70 @@ internal class ShoppingListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun loadShoppingListData(): ShoppingListData = coroutineScope {
|
||||||
|
val foodsDeferred = async {
|
||||||
|
runCatchingExceptCancel {
|
||||||
|
shoppingListsRepo.getFoods()
|
||||||
|
}.getOrDefault(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
val unitsDeferred = async {
|
||||||
|
runCatchingExceptCancel {
|
||||||
|
shoppingListsRepo.getUnits()
|
||||||
|
}.getOrDefault(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
val shoppingListDeferred = async {
|
||||||
|
shoppingListsRepo.getShoppingList(args.shoppingListId)
|
||||||
|
}
|
||||||
|
|
||||||
|
ShoppingListData(
|
||||||
|
foods = foodsDeferred.await(),
|
||||||
|
units = unitsDeferred.await(),
|
||||||
|
shoppingList = shoppingListDeferred.await(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun doRefresh() {
|
private suspend fun doRefresh() {
|
||||||
_errorToShowInSnackbar = loadingHelper.refresh().exceptionOrNull()
|
_errorToShowInSnackbar = loadingHelper.refresh().exceptionOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildLoadingState(
|
private fun buildLoadingState(
|
||||||
loadingState: LoadingState<FullShoppingListInfo>,
|
loadingState: LoadingState<ShoppingListData>,
|
||||||
checkedOverrideMap: Map<String, Boolean>,
|
editingState: ShoppingListEditingState,
|
||||||
deletedItemIds: Set<String>,
|
|
||||||
): LoadingState<ShoppingListScreenState> {
|
): LoadingState<ShoppingListScreenState> {
|
||||||
logger.v { "buildLoadingState() called with: loadingState = $loadingState, checkedOverrideMap = $checkedOverrideMap, deletedItemIds = $deletedItemIds" }
|
logger.v { "buildLoadingState() called with: loadingState = $loadingState, editingState = $editingState" }
|
||||||
return loadingState.map { shoppingList ->
|
return loadingState.map { data ->
|
||||||
val items = shoppingList.items
|
val existingItems = data.shoppingList.items
|
||||||
.filter { it.id !in deletedItemIds }
|
.filter { it.id !in editingState.deletedItemIds }
|
||||||
.map { it.copy(checked = checkedOverrideMap[it.id] ?: it.checked) }
|
.map {
|
||||||
.sortedBy { it.checked }
|
ShoppingListItemState.ExistingItem(
|
||||||
ShoppingListScreenState(name = shoppingList.name, items = items)
|
item = editingState.modifiedItems[it.id] ?: it,
|
||||||
|
isEditing = it.id in editingState.editingItemIds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val items = (existingItems + editingState.newItems).sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ it.checked },
|
||||||
|
{ it.position },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ShoppingListScreenState(
|
||||||
|
name = data.shoppingList.name,
|
||||||
|
listId = data.shoppingList.id,
|
||||||
|
items = items,
|
||||||
|
foods = data.foods.sortedBy { it.name },
|
||||||
|
units = data.units.sortedBy { it.name },
|
||||||
|
)
|
||||||
|
}.also {
|
||||||
|
logger.v { "buildLoadingState() returned: $it" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onItemCheckedChange(item: ShoppingListItemInfo, isChecked: Boolean) {
|
fun onItemCheckedChange(state: ShoppingListItemState.ExistingItem, isChecked: Boolean) {
|
||||||
logger.v { "onItemCheckedChange() called with: item = $item, isChecked = $isChecked" }
|
logger.v { "onItemCheckedChange() called with: state = $state, isChecked = $isChecked" }
|
||||||
viewModelScope.launch {
|
val updatedItem = state.item.copy(checked = isChecked)
|
||||||
checkedOverride.update { originalMap ->
|
updateItemInformation(updatedItem)
|
||||||
originalMap.toMutableMap().also { newMap -> newMap[item.id] = isChecked }
|
|
||||||
}
|
|
||||||
checkedOverride.value[item.id] = isChecked
|
|
||||||
val result = runCatchingExceptCancel {
|
|
||||||
shoppingListsRepo.updateIsShoppingListItemChecked(item.id, isChecked)
|
|
||||||
}.onFailure {
|
|
||||||
logger.e(it) { "Failed to update item's checked state" }
|
|
||||||
}
|
|
||||||
_errorToShowInSnackbar = result.exceptionOrNull()
|
|
||||||
if (result.isSuccess) {
|
|
||||||
logger.v { "Item's checked state updated" }
|
|
||||||
doRefresh()
|
|
||||||
}
|
|
||||||
checkedOverride.update { originalMap ->
|
|
||||||
originalMap.toMutableMap().also { newMap -> newMap.remove(item.id) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSnackbarShown() {
|
fun onSnackbarShown() {
|
||||||
@@ -126,10 +150,13 @@ internal class ShoppingListViewModel @Inject constructor(
|
|||||||
_errorToShowInSnackbar = null
|
_errorToShowInSnackbar = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteShoppingListItem(item: ShoppingListItemInfo) {
|
fun deleteShoppingListItem(state: ShoppingListItemState.ExistingItem) {
|
||||||
logger.v { "deleteShoppingListItem() called with: item = $item" }
|
logger.v { "deleteShoppingListItem() called with: state = $state" }
|
||||||
|
val item = state.item
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
deletedItemIds.update { it + item.id }
|
editingStateFlow.update {
|
||||||
|
it.copy(deletedItemIds = it.deletedItemIds + item.id)
|
||||||
|
}
|
||||||
val result = runCatchingExceptCancel {
|
val result = runCatchingExceptCancel {
|
||||||
shoppingListsRepo.deleteShoppingListItem(item.id)
|
shoppingListsRepo.deleteShoppingListItem(item.id)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
@@ -140,7 +167,121 @@ internal class ShoppingListViewModel @Inject constructor(
|
|||||||
logger.v { "Item deleted" }
|
logger.v { "Item deleted" }
|
||||||
doRefresh()
|
doRefresh()
|
||||||
}
|
}
|
||||||
deletedItemIds.update { it - item.id }
|
editingStateFlow.update {
|
||||||
|
it.copy(deletedItemIds = it.deletedItemIds - item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditStart(state: ShoppingListItemState.ExistingItem) {
|
||||||
|
logger.v { "onEditStart() called with: state = $state" }
|
||||||
|
val item = state.item
|
||||||
|
editingStateFlow.update {
|
||||||
|
it.copy(editingItemIds = it.editingItemIds + item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditCancel(state: ShoppingListItemState.ExistingItem) {
|
||||||
|
logger.v { "onEditCancel() called with: state = $state" }
|
||||||
|
val item = state.item
|
||||||
|
editingStateFlow.update {
|
||||||
|
it.copy(editingItemIds = it.editingItemIds - item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditConfirm(
|
||||||
|
state: ShoppingListItemState.ExistingItem,
|
||||||
|
input: ShoppingListItemEditorState
|
||||||
|
) {
|
||||||
|
logger.v { "onEditConfirm() called with: state = $state, input = $input" }
|
||||||
|
val id = state.item.id
|
||||||
|
editingStateFlow.update {
|
||||||
|
it.copy(editingItemIds = it.editingItemIds - id)
|
||||||
|
}
|
||||||
|
val updatedItem = state.item.copy(
|
||||||
|
note = input.note,
|
||||||
|
quantity = input.quantity.toDouble(),
|
||||||
|
isFood = input.isFood,
|
||||||
|
unit = input.unit,
|
||||||
|
food = input.food,
|
||||||
|
)
|
||||||
|
updateItemInformation(updatedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateItemInformation(updatedItem: ShoppingListItemInfo) {
|
||||||
|
logger.v { "updateItemInformation() called with: updatedItem = $updatedItem" }
|
||||||
|
val id = updatedItem.id
|
||||||
|
viewModelScope.launch {
|
||||||
|
editingStateFlow.update { state ->
|
||||||
|
state.copy(modifiedItems = state.modifiedItems + (id to updatedItem))
|
||||||
|
}
|
||||||
|
val result = runCatchingExceptCancel {
|
||||||
|
shoppingListsRepo.updateShoppingListItem(updatedItem)
|
||||||
|
}.onFailure {
|
||||||
|
logger.e(it) { "Failed to update item" }
|
||||||
|
}
|
||||||
|
_errorToShowInSnackbar = result.exceptionOrNull()
|
||||||
|
if (result.isSuccess) {
|
||||||
|
logger.v { "Item updated" }
|
||||||
|
doRefresh()
|
||||||
|
}
|
||||||
|
editingStateFlow.update { state ->
|
||||||
|
state.copy(modifiedItems = state.modifiedItems - id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddItemClicked() {
|
||||||
|
logger.v { "onAddItemClicked() called" }
|
||||||
|
val shoppingListScreenState = loadingState.value.data ?: return
|
||||||
|
val maxPosition = shoppingListScreenState.items.maxOfOrNull { it.position } ?: 0
|
||||||
|
val editorState = ShoppingListItemEditorState(
|
||||||
|
foods = shoppingListScreenState.foods,
|
||||||
|
units = shoppingListScreenState.units,
|
||||||
|
position = maxPosition + 1,
|
||||||
|
listId = shoppingListScreenState.listId,
|
||||||
|
)
|
||||||
|
val item = ShoppingListItemState.NewItem(editorState)
|
||||||
|
editingStateFlow.update {
|
||||||
|
it.copy(newItems = it.newItems + item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddCancel(state: ShoppingListItemState.NewItem) {
|
||||||
|
logger.v { "onAddCancel() called with: state = $state" }
|
||||||
|
editingStateFlow.update {
|
||||||
|
it.copy(newItems = it.newItems - state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAddConfirm(
|
||||||
|
state: ShoppingListItemState.NewItem,
|
||||||
|
) {
|
||||||
|
logger.v { "onAddConfirm() called with: state = $state" }
|
||||||
|
val item = state.item
|
||||||
|
val newItem = NewShoppingListItemInfo(
|
||||||
|
shoppingListId = item.listId,
|
||||||
|
note = item.note,
|
||||||
|
quantity = item.quantity.toDouble(),
|
||||||
|
isFood = item.isFood,
|
||||||
|
unit = item.unit,
|
||||||
|
food = item.food,
|
||||||
|
position = item.position,
|
||||||
|
)
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = runCatchingExceptCancel {
|
||||||
|
shoppingListsRepo.addShoppingListItem(newItem)
|
||||||
|
}.onFailure {
|
||||||
|
logger.e(it) { "Failed to add item" }
|
||||||
|
}
|
||||||
|
_errorToShowInSnackbar = result.exceptionOrNull()
|
||||||
|
if (result.isSuccess) {
|
||||||
|
logger.v { "Item added" }
|
||||||
|
doRefresh()
|
||||||
|
}
|
||||||
|
editingStateFlow.update {
|
||||||
|
it.copy(newItems = it.newItems - state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
@@ -32,6 +33,8 @@ fun <T> LazyColumnWithLoadingState(
|
|||||||
errorToShowInSnackbar: Throwable? = null,
|
errorToShowInSnackbar: Throwable? = null,
|
||||||
onSnackbarShown: () -> Unit = {},
|
onSnackbarShown: () -> Unit = {},
|
||||||
onRefresh: () -> Unit = {},
|
onRefresh: () -> Unit = {},
|
||||||
|
floatingActionButton: @Composable () -> Unit = {},
|
||||||
|
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
||||||
lazyColumnContent: LazyListScope.(List<T>) -> Unit = {},
|
lazyColumnContent: LazyListScope.(List<T>) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val refreshState = rememberPullRefreshState(
|
val refreshState = rememberPullRefreshState(
|
||||||
@@ -42,6 +45,8 @@ fun <T> LazyColumnWithLoadingState(
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
floatingActionButton = floatingActionButton,
|
||||||
|
floatingActionButtonPosition = floatingActionButtonPosition,
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
val innerModifier = Modifier
|
val innerModifier = Modifier
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="shopping_lists_screen_cart_icon">Shopping cart</string>
|
<string name="shopping_lists_screen_cart_icon">Shopping cart</string>
|
||||||
<string name="shopping_list_screen_unknown_error">Unknown error</string>
|
<string name="shopping_list_screen_unknown_error">Unknown error</string>
|
||||||
<string name="shopping_list_screen_empty_list">%1$s is empty</string>
|
<string name="shopping_list_screen_empty_list">%1$s is empty</string>
|
||||||
<string name="shopping_list_screen_delete_icon_content_description">Delete</string>
|
<string name="shopping_list_screen_delete_icon_content_description">Delete</string>
|
||||||
<string name="shopping_lists_screen_empty">No shopping lists found</string>
|
<string name="shopping_list_screen_editor_quantity_label">Qty</string>
|
||||||
<string name="shopping_lists_screen_unauthorized_error">Authentication is required</string>
|
<string name="shopping_list_screen_editor_note_label">Note</string>
|
||||||
<string name="shopping_lists_screen_no_connection">No server connection</string>
|
<string name="shopping_list_screen_editor_food_label">Food</string>
|
||||||
<string name="shopping_lists_screen_unknown_error">Unknown error</string>
|
<string name="shopping_list_screen_editor_unit_label">Unit</string>
|
||||||
<string name="shopping_lists_screen_empty_button_refresh">Try again</string>
|
<string name="shopping_list_screen_editor_save_button">Save</string>
|
||||||
|
<string name="shopping_list_screen_editor_not_food_button">Not food</string>
|
||||||
|
<string name="shopping_list_screen_editor_food_button">Food</string>
|
||||||
|
<string name="shopping_list_screen_editor_cancel_button">Cancel</string>
|
||||||
|
<string name="shopping_list_screen_edit_icon_content_description">Edit</string>
|
||||||
|
<string name="shopping_list_screen_editor_checked_unit_content_description">Selected unit</string>
|
||||||
|
<string name="shopping_list_screen_editor_checked_food_content_description">Selected food</string>
|
||||||
|
<string name="shopping_list_screen_add_icon_content_description">Add item</string>
|
||||||
|
|
||||||
|
<string name="shopping_lists_screen_empty">No shopping lists found</string>
|
||||||
|
<string name="shopping_lists_screen_unauthorized_error">Authentication is required</string>
|
||||||
|
<string name="shopping_lists_screen_no_connection">No server connection</string>
|
||||||
|
<string name="shopping_lists_screen_unknown_error">Unknown error</string>
|
||||||
|
<string name="shopping_lists_screen_empty_button_refresh">Try again</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -103,6 +103,7 @@ android-material-material = { group = "com.google.android.material", name = "mat
|
|||||||
|
|
||||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
androidx-compose-materialIconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||||
androidx-compose-ui-toolingPreview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
androidx-compose-ui-toolingPreview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||||
androidx-compose-ui-testJunit = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-compose-ui-testJunit = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import gq.kirmanak.mealient.datasource.models.AddRecipeInfo
|
|||||||
import gq.kirmanak.mealient.datasource.models.AddRecipeIngredientInfo
|
import gq.kirmanak.mealient.datasource.models.AddRecipeIngredientInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.AddRecipeInstructionInfo
|
import gq.kirmanak.mealient.datasource.models.AddRecipeInstructionInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.AddRecipeSettingsInfo
|
import gq.kirmanak.mealient.datasource.models.AddRecipeSettingsInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.FullRecipeInfo
|
import gq.kirmanak.mealient.datasource.models.FullRecipeInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
|
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo
|
import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo
|
import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo
|
||||||
@@ -19,6 +21,7 @@ import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
|
|||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemRecipeReferenceInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemRecipeReferenceInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListsInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListsInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.VersionInfo
|
import gq.kirmanak.mealient.datasource.models.VersionInfo
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0
|
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0
|
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0
|
||||||
@@ -34,6 +37,8 @@ import gq.kirmanak.mealient.datasource.v1.models.AddRecipeIngredientV1
|
|||||||
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1
|
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1
|
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.CreateShoppingListItemRequestV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetFoodsResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
||||||
@@ -44,6 +49,7 @@ import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListItemResponseV1
|
|||||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsSummaryResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsSummaryResponseV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetUnitsResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
|
||||||
@@ -127,4 +133,10 @@ interface ModelMapper {
|
|||||||
fun toV0Instruction(addRecipeInstructionInfo: AddRecipeInstructionInfo): AddRecipeInstructionV0
|
fun toV0Instruction(addRecipeInstructionInfo: AddRecipeInstructionInfo): AddRecipeInstructionV0
|
||||||
|
|
||||||
fun toV0Request(parseRecipeURLInfo: ParseRecipeURLInfo): ParseRecipeURLRequestV0
|
fun toV0Request(parseRecipeURLInfo: ParseRecipeURLInfo): ParseRecipeURLRequestV0
|
||||||
|
|
||||||
|
fun toFoodInfo(getFoodsResponseV1: GetFoodsResponseV1): List<FoodInfo>
|
||||||
|
|
||||||
|
fun toUnitInfo(getUnitsResponseV1: GetUnitsResponseV1): List<UnitInfo>
|
||||||
|
|
||||||
|
fun toV1CreateRequest(addRecipeInfo: NewShoppingListItemInfo): CreateShoppingListItemRequestV1
|
||||||
}
|
}
|
||||||
@@ -8,8 +8,10 @@ import gq.kirmanak.mealient.datasource.models.AddRecipeInfo
|
|||||||
import gq.kirmanak.mealient.datasource.models.AddRecipeIngredientInfo
|
import gq.kirmanak.mealient.datasource.models.AddRecipeIngredientInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.AddRecipeInstructionInfo
|
import gq.kirmanak.mealient.datasource.models.AddRecipeInstructionInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.AddRecipeSettingsInfo
|
import gq.kirmanak.mealient.datasource.models.AddRecipeSettingsInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.FullRecipeInfo
|
import gq.kirmanak.mealient.datasource.models.FullRecipeInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
import gq.kirmanak.mealient.datasource.models.FullShoppingListInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
|
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo
|
import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo
|
import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo
|
||||||
@@ -19,6 +21,7 @@ import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
|
|||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemRecipeReferenceInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListItemRecipeReferenceInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.ShoppingListsInfo
|
import gq.kirmanak.mealient.datasource.models.ShoppingListsInfo
|
||||||
|
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||||
import gq.kirmanak.mealient.datasource.models.VersionInfo
|
import gq.kirmanak.mealient.datasource.models.VersionInfo
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0
|
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0
|
||||||
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0
|
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0
|
||||||
@@ -34,7 +37,12 @@ import gq.kirmanak.mealient.datasource.v1.models.AddRecipeIngredientV1
|
|||||||
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1
|
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1
|
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.CreateShoppingListItemRequestV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetFoodResponseV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetFoodsResponseV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientFoodResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientUnitResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSettingsResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSettingsResponseV1
|
||||||
@@ -44,6 +52,8 @@ import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListItemResponseV1
|
|||||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsSummaryResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsSummaryResponseV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetUnitResponseV1
|
||||||
|
import gq.kirmanak.mealient.datasource.v1.models.GetUnitsResponseV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
|
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
|
||||||
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
|
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
|
||||||
@@ -198,12 +208,26 @@ class ModelMapperImpl @Inject constructor() : ModelMapper {
|
|||||||
isFood = getShoppingListItemResponseV1.isFood,
|
isFood = getShoppingListItemResponseV1.isFood,
|
||||||
note = getShoppingListItemResponseV1.note,
|
note = getShoppingListItemResponseV1.note,
|
||||||
quantity = getShoppingListItemResponseV1.quantity,
|
quantity = getShoppingListItemResponseV1.quantity,
|
||||||
unit = getShoppingListItemResponseV1.unit?.name.orEmpty(),
|
unit = getShoppingListItemResponseV1.unit?.let { toUnitInfo(it) },
|
||||||
food = getShoppingListItemResponseV1.food?.name.orEmpty(),
|
food = getShoppingListItemResponseV1.food?.let { toFoodInfo(it) },
|
||||||
recipeReferences = getShoppingListItemResponseV1.recipeReferences.map { it.recipeId }
|
recipeReferences = getShoppingListItemResponseV1.recipeReferences.map { it.recipeId }
|
||||||
.mapNotNull { recipes[it] }.flatten().map { toShoppingListItemRecipeReferenceInfo(it) },
|
.mapNotNull { recipes[it] }.flatten().map { toShoppingListItemRecipeReferenceInfo(it) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun toUnitInfo(getRecipeIngredientUnitResponseV1: GetRecipeIngredientUnitResponseV1): UnitInfo {
|
||||||
|
return UnitInfo(
|
||||||
|
name = getRecipeIngredientUnitResponseV1.name,
|
||||||
|
id = getRecipeIngredientUnitResponseV1.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toFoodInfo(getRecipeIngredientFoodResponseV1: GetRecipeIngredientFoodResponseV1): FoodInfo {
|
||||||
|
return FoodInfo(
|
||||||
|
name = getRecipeIngredientFoodResponseV1.name,
|
||||||
|
id = getRecipeIngredientFoodResponseV1.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun toShoppingListItemRecipeReferenceInfo(
|
override fun toShoppingListItemRecipeReferenceInfo(
|
||||||
getShoppingListItemRecipeReferenceFullResponseV1: GetShoppingListItemRecipeReferenceFullResponseV1
|
getShoppingListItemRecipeReferenceFullResponseV1: GetShoppingListItemRecipeReferenceFullResponseV1
|
||||||
) = ShoppingListItemRecipeReferenceInfo(
|
) = ShoppingListItemRecipeReferenceInfo(
|
||||||
@@ -305,4 +329,39 @@ class ModelMapperImpl @Inject constructor() : ModelMapper {
|
|||||||
override fun toV0Request(parseRecipeURLInfo: ParseRecipeURLInfo) = ParseRecipeURLRequestV0(
|
override fun toV0Request(parseRecipeURLInfo: ParseRecipeURLInfo) = ParseRecipeURLRequestV0(
|
||||||
url = parseRecipeURLInfo.url,
|
url = parseRecipeURLInfo.url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override fun toFoodInfo(getFoodsResponseV1: GetFoodsResponseV1): List<FoodInfo> {
|
||||||
|
return getFoodsResponseV1.items.map { toFoodInfo(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toFoodInfo(getFoodResponseV1: GetFoodResponseV1): FoodInfo {
|
||||||
|
return FoodInfo(
|
||||||
|
name = getFoodResponseV1.name,
|
||||||
|
id = getFoodResponseV1.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toUnitInfo(getUnitsResponseV1: GetUnitsResponseV1): List<UnitInfo> {
|
||||||
|
return getUnitsResponseV1.items.map { toUnitInfo(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toUnitInfo(getUnitResponseV1: GetUnitResponseV1): UnitInfo {
|
||||||
|
return UnitInfo(
|
||||||
|
name = getUnitResponseV1.name,
|
||||||
|
id = getUnitResponseV1.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toV1CreateRequest(addRecipeInfo: NewShoppingListItemInfo): CreateShoppingListItemRequestV1 {
|
||||||
|
return CreateShoppingListItemRequestV1(
|
||||||
|
shoppingListId = addRecipeInfo.shoppingListId,
|
||||||
|
checked = false,
|
||||||
|
position = addRecipeInfo.position,
|
||||||
|
isFood = addRecipeInfo.isFood,
|
||||||
|
note = addRecipeInfo.note,
|
||||||
|
quantity = addRecipeInfo.quantity,
|
||||||
|
foodId = addRecipeInfo.food?.id,
|
||||||
|
unitId = addRecipeInfo.unit?.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user