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 note: String,
|
||||
val quantity: Double,
|
||||
val unit: String,
|
||||
val food: String,
|
||||
val unit: UnitInfo?,
|
||||
val food: FoodInfo?,
|
||||
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
|
||||
|
||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
|
||||
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.GetRecipeSummaryResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
|
||||
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.ParseRecipeURLRequestV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
|
||||
@@ -63,7 +67,13 @@ interface MealieDataSourceV1 {
|
||||
|
||||
suspend fun getShoppingList(id: String): GetShoppingListResponseV1
|
||||
|
||||
suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean)
|
||||
|
||||
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.NetworkRequestWrapper
|
||||
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.CreateApiTokenResponseV1
|
||||
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.GetFoodsResponseV1
|
||||
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.GetUnitsResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
|
||||
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.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
|
||||
@@ -175,20 +177,6 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
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(
|
||||
id: String,
|
||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
@@ -196,4 +184,44 @@ class MealieDataSourceV1Impl @Inject constructor(
|
||||
logMethod = { "deleteShoppingListItem" },
|
||||
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(
|
||||
@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
|
||||
data class GetRecipeIngredientFoodResponseV1(
|
||||
@SerialName("name") val name: String = "",
|
||||
@SerialName("id") val id: String = "",
|
||||
)
|
||||
@@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class GetRecipeIngredientUnitResponseV1(
|
||||
@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.androidx.compose.material)
|
||||
implementation(libs.androidx.compose.materialIconsExtended)
|
||||
|
||||
implementation(libs.google.dagger.hiltAndroid)
|
||||
kapt(libs.google.dagger.hiltCompiler)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
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.NewShoppingListItemInfo
|
||||
import gq.kirmanak.mealient.datasource.models.ShoppingListInfo
|
||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||
|
||||
interface ShoppingListsDataSource {
|
||||
|
||||
@@ -9,7 +13,13 @@ interface ShoppingListsDataSource {
|
||||
|
||||
suspend fun getShoppingList(id: String): FullShoppingListInfo
|
||||
|
||||
suspend fun updateIsShoppingListItemChecked(id: String, checked: Boolean)
|
||||
|
||||
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
|
||||
|
||||
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||
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.ShoppingListItemInfo
|
||||
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
|
||||
import gq.kirmanak.mealient.model_mapper.ModelMapper
|
||||
import javax.inject.Inject
|
||||
@@ -20,14 +24,20 @@ class ShoppingListsDataSourceImpl @Inject constructor(
|
||||
id: String
|
||||
): FullShoppingListInfo = modelMapper.toFullShoppingListInfo(v1Source.getShoppingList(id))
|
||||
|
||||
override suspend fun updateIsShoppingListItemChecked(
|
||||
id: String,
|
||||
checked: Boolean,
|
||||
) = v1Source.updateIsShoppingListItemChecked(id, checked)
|
||||
|
||||
override suspend fun deleteShoppingListItem(
|
||||
id: String
|
||||
) = 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
|
||||
|
||||
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||
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.ShoppingListItemInfo
|
||||
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||
|
||||
interface ShoppingListsRepo {
|
||||
|
||||
suspend fun updateIsShoppingListItemChecked(id: String, isChecked: Boolean)
|
||||
|
||||
suspend fun getShoppingLists(): List<ShoppingListInfo>
|
||||
|
||||
suspend fun getShoppingList(id: String): FullShoppingListInfo
|
||||
|
||||
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
|
||||
|
||||
import gq.kirmanak.mealient.datasource.models.FoodInfo
|
||||
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.ShoppingListItemInfo
|
||||
import gq.kirmanak.mealient.datasource.models.UnitInfo
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import gq.kirmanak.mealient.shopping_lists.network.ShoppingListsDataSource
|
||||
import javax.inject.Inject
|
||||
@@ -11,11 +15,6 @@ class ShoppingListsRepoImpl @Inject constructor(
|
||||
private val logger: Logger,
|
||||
) : 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> {
|
||||
logger.v { "getShoppingLists() called" }
|
||||
return dataSource.getAllShoppingLists()
|
||||
@@ -30,4 +29,24 @@ class ShoppingListsRepoImpl @Inject constructor(
|
||||
logger.v { "deleteShoppingListItem() called with: id = $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.padding
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
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.Edit
|
||||
import androidx.compose.material.icons.filled.NoMeals
|
||||
import androidx.compose.material.icons.filled.Restaurant
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DismissDirection
|
||||
import androidx.compose.material3.DismissState
|
||||
import androidx.compose.material3.DismissValue
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
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.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.SwipeToDismiss
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberDismissState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import gq.kirmanak.mealient.AppTheme
|
||||
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.RecipeIngredientInfo
|
||||
import gq.kirmanak.mealient.datasource.models.RecipeInstructionInfo
|
||||
import gq.kirmanak.mealient.datasource.models.RecipeSettingsInfo
|
||||
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
|
||||
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_lists.ui.composables.LazyColumnWithLoadingState
|
||||
import gq.kirmanak.mealient.shopping_lists.util.data
|
||||
import gq.kirmanak.mealient.shopping_lists.util.map
|
||||
import kotlinx.coroutines.android.awaitFrame
|
||||
import java.text.DecimalFormat
|
||||
|
||||
data class ShoppingListNavArgs(
|
||||
val shoppingListId: String,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination(
|
||||
navArgsDelegate = ShoppingListNavArgs::class,
|
||||
)
|
||||
@@ -65,54 +89,415 @@ internal fun ShoppingListScreen(
|
||||
|
||||
LazyColumnWithLoadingState(
|
||||
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),
|
||||
defaultEmptyListError = defaultEmptyListError,
|
||||
errorToShowInSnackbar = shoppingListViewModel.errorToShowInSnackbar,
|
||||
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 ->
|
||||
val firstCheckedItemIndex = items.indexOfFirst { it.checked }
|
||||
|
||||
itemsIndexed(items, { _, item -> item.id }) { index, item ->
|
||||
ShoppingListItem(
|
||||
shoppingListItem = item,
|
||||
showDivider = index == firstCheckedItemIndex && index != 0,
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||
onCheckedChange = { isChecked ->
|
||||
shoppingListViewModel.onItemCheckedChange(item, isChecked)
|
||||
},
|
||||
onDismissed = {
|
||||
shoppingListViewModel.deleteShoppingListItem(item)
|
||||
itemsIndexed(items, { _, item -> item.id }) { index, itemState ->
|
||||
if (itemState is ShoppingListItemState.ExistingItem) {
|
||||
if (itemState.isEditing) {
|
||||
val state = remember {
|
||||
ShoppingListItemEditorState(
|
||||
state = itemState,
|
||||
foods = loadingState.data?.foods.orEmpty(),
|
||||
units = loadingState.data?.units.orEmpty(),
|
||||
)
|
||||
}
|
||||
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)
|
||||
@Composable
|
||||
fun ShoppingListItem(
|
||||
shoppingListItem: ShoppingListItemInfo,
|
||||
itemState: ShoppingListItemState.ExistingItem,
|
||||
showDivider: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onCheckedChange: (Boolean) -> Unit = {},
|
||||
onDismissed: () -> Unit = {},
|
||||
) {
|
||||
val dismissState = rememberDismissState(
|
||||
onEditStart: () -> Unit = {},
|
||||
dismissState: DismissState = rememberDismissState(
|
||||
confirmValueChange = {
|
||||
if (it == DismissValue.DismissedToStart) {
|
||||
onDismissed()
|
||||
when (it) {
|
||||
DismissValue.DismissedToStart -> onDismissed()
|
||||
DismissValue.DismissedToEnd -> onEditStart()
|
||||
DismissValue.Default -> Unit
|
||||
}
|
||||
true
|
||||
}
|
||||
)
|
||||
) {
|
||||
val shoppingListItem = itemState.item
|
||||
SwipeToDismiss(
|
||||
state = dismissState,
|
||||
background = {
|
||||
if (dismissState.targetValue == DismissValue.DismissedToStart) {
|
||||
val color by animateColorAsState(MaterialTheme.colorScheme.error)
|
||||
val iconColor by animateColorAsState(MaterialTheme.colorScheme.onSurface)
|
||||
val iconColor by animateColorAsState(MaterialTheme.colorScheme.onError)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -127,6 +512,23 @@ fun ShoppingListItem(
|
||||
.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 = {
|
||||
@@ -143,7 +545,7 @@ fun ShoppingListItem(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
Checkbox(
|
||||
checked = shoppingListItem.checked,
|
||||
checked = itemState.item.checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
|
||||
@@ -154,8 +556,8 @@ fun ShoppingListItem(
|
||||
?.let { DecimalFormat.getInstance().format(it) }
|
||||
val text = listOfNotNull(
|
||||
quantity,
|
||||
shoppingListItem.unit.takeIf { isFood },
|
||||
shoppingListItem.food.takeIf { isFood },
|
||||
shoppingListItem.unit.takeIf { isFood }?.name,
|
||||
shoppingListItem.food.takeIf { isFood }?.name,
|
||||
shoppingListItem.note,
|
||||
).filter { it.isNotBlank() }.joinToString(" ")
|
||||
|
||||
@@ -164,23 +566,60 @@ fun ShoppingListItem(
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
directions = setOf(DismissDirection.EndToStart),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@Preview
|
||||
fun PreviewShoppingListItemChecked() {
|
||||
AppTheme {
|
||||
ShoppingListItem(shoppingListItem = PreviewData.milk, false)
|
||||
ShoppingListItem(
|
||||
itemState = ShoppingListItemState.ExistingItem(PreviewData.milk),
|
||||
showDivider = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@Preview
|
||||
fun PreviewShoppingListItemUnchecked() {
|
||||
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,
|
||||
note = "Black tea bags",
|
||||
quantity = 30.0,
|
||||
unit = "",
|
||||
food = "",
|
||||
unit = null,
|
||||
food = null,
|
||||
recipeReferences = listOf(
|
||||
ShoppingListItemRecipeReferenceInfo(
|
||||
shoppingListId = "1",
|
||||
@@ -243,12 +682,12 @@ private object PreviewData {
|
||||
id = "2",
|
||||
shoppingListId = "1",
|
||||
checked = true,
|
||||
position = 1,
|
||||
position = 0,
|
||||
isFood = true,
|
||||
note = "Cold",
|
||||
quantity = 500.0,
|
||||
unit = "ml",
|
||||
food = "Milk",
|
||||
unit = UnitInfo("ml", ""),
|
||||
food = FoodInfo("Milk", ""),
|
||||
recipeReferences = listOf(
|
||||
ShoppingListItemRecipeReferenceInfo(
|
||||
shoppingListId = "1",
|
||||
|
||||
@@ -1,8 +1,45 @@
|
||||
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.UnitInfo
|
||||
import java.util.UUID
|
||||
|
||||
internal data class ShoppingListScreenState(
|
||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||
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.runCatchingExceptCancel
|
||||
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.LoadingState
|
||||
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 kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -39,18 +42,15 @@ internal class ShoppingListViewModel @Inject constructor(
|
||||
|
||||
private val args: ShoppingListNavArgs = ShoppingListScreenDestination.argsFrom(savedStateHandle)
|
||||
|
||||
private val checkedOverride = MutableStateFlow<MutableMap<String, Boolean>>(mutableMapOf())
|
||||
|
||||
private val deletedItemIds = MutableStateFlow<Set<String>>(mutableSetOf())
|
||||
private val editingStateFlow = MutableStateFlow(ShoppingListEditingState())
|
||||
|
||||
private val loadingHelper = loadingHelperFactory.create(viewModelScope) {
|
||||
shoppingListsRepo.getShoppingList(args.shoppingListId)
|
||||
loadShoppingListData()
|
||||
}
|
||||
|
||||
val loadingState: StateFlow<LoadingState<ShoppingListScreenState>> = combine(
|
||||
loadingHelper.loadingState,
|
||||
checkedOverride,
|
||||
deletedItemIds,
|
||||
editingStateFlow,
|
||||
::buildLoadingState,
|
||||
).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() {
|
||||
_errorToShowInSnackbar = loadingHelper.refresh().exceptionOrNull()
|
||||
}
|
||||
|
||||
private fun buildLoadingState(
|
||||
loadingState: LoadingState<FullShoppingListInfo>,
|
||||
checkedOverrideMap: Map<String, Boolean>,
|
||||
deletedItemIds: Set<String>,
|
||||
loadingState: LoadingState<ShoppingListData>,
|
||||
editingState: ShoppingListEditingState,
|
||||
): LoadingState<ShoppingListScreenState> {
|
||||
logger.v { "buildLoadingState() called with: loadingState = $loadingState, checkedOverrideMap = $checkedOverrideMap, deletedItemIds = $deletedItemIds" }
|
||||
return loadingState.map { shoppingList ->
|
||||
val items = shoppingList.items
|
||||
.filter { it.id !in deletedItemIds }
|
||||
.map { it.copy(checked = checkedOverrideMap[it.id] ?: it.checked) }
|
||||
.sortedBy { it.checked }
|
||||
ShoppingListScreenState(name = shoppingList.name, items = items)
|
||||
logger.v { "buildLoadingState() called with: loadingState = $loadingState, editingState = $editingState" }
|
||||
return loadingState.map { data ->
|
||||
val existingItems = data.shoppingList.items
|
||||
.filter { it.id !in editingState.deletedItemIds }
|
||||
.map {
|
||||
ShoppingListItemState.ExistingItem(
|
||||
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) {
|
||||
logger.v { "onItemCheckedChange() called with: item = $item, isChecked = $isChecked" }
|
||||
viewModelScope.launch {
|
||||
checkedOverride.update { originalMap ->
|
||||
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 onItemCheckedChange(state: ShoppingListItemState.ExistingItem, isChecked: Boolean) {
|
||||
logger.v { "onItemCheckedChange() called with: state = $state, isChecked = $isChecked" }
|
||||
val updatedItem = state.item.copy(checked = isChecked)
|
||||
updateItemInformation(updatedItem)
|
||||
}
|
||||
|
||||
fun onSnackbarShown() {
|
||||
@@ -126,10 +150,13 @@ internal class ShoppingListViewModel @Inject constructor(
|
||||
_errorToShowInSnackbar = null
|
||||
}
|
||||
|
||||
fun deleteShoppingListItem(item: ShoppingListItemInfo) {
|
||||
logger.v { "deleteShoppingListItem() called with: item = $item" }
|
||||
fun deleteShoppingListItem(state: ShoppingListItemState.ExistingItem) {
|
||||
logger.v { "deleteShoppingListItem() called with: state = $state" }
|
||||
val item = state.item
|
||||
viewModelScope.launch {
|
||||
deletedItemIds.update { it + item.id }
|
||||
editingStateFlow.update {
|
||||
it.copy(deletedItemIds = it.deletedItemIds + item.id)
|
||||
}
|
||||
val result = runCatchingExceptCancel {
|
||||
shoppingListsRepo.deleteShoppingListItem(item.id)
|
||||
}.onFailure {
|
||||
@@ -140,7 +167,121 @@ internal class ShoppingListViewModel @Inject constructor(
|
||||
logger.v { "Item deleted" }
|
||||
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.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
@@ -32,6 +33,8 @@ fun <T> LazyColumnWithLoadingState(
|
||||
errorToShowInSnackbar: Throwable? = null,
|
||||
onSnackbarShown: () -> Unit = {},
|
||||
onRefresh: () -> Unit = {},
|
||||
floatingActionButton: @Composable () -> Unit = {},
|
||||
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
||||
lazyColumnContent: LazyListScope.(List<T>) -> Unit = {},
|
||||
) {
|
||||
val refreshState = rememberPullRefreshState(
|
||||
@@ -42,6 +45,8 @@ fun <T> LazyColumnWithLoadingState(
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
floatingActionButton = floatingActionButton,
|
||||
floatingActionButtonPosition = floatingActionButtonPosition,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
val innerModifier = Modifier
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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_empty_list">%1$s is empty</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_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>
|
||||
<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_empty_list">%1$s is empty</string>
|
||||
<string name="shopping_list_screen_delete_icon_content_description">Delete</string>
|
||||
<string name="shopping_list_screen_editor_quantity_label">Qty</string>
|
||||
<string name="shopping_list_screen_editor_note_label">Note</string>
|
||||
<string name="shopping_list_screen_editor_food_label">Food</string>
|
||||
<string name="shopping_list_screen_editor_unit_label">Unit</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>
|
||||
@@ -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-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-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
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.AddRecipeInstructionInfo
|
||||
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.FullShoppingListInfo
|
||||
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
|
||||
import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo
|
||||
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.ShoppingListItemRecipeReferenceInfo
|
||||
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.v0.models.AddRecipeIngredientV0
|
||||
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.AddRecipeSettingsV1
|
||||
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.GetRecipeInstructionResponseV1
|
||||
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.GetShoppingListsResponseV1
|
||||
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.UpdateRecipeRequestV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
|
||||
@@ -127,4 +133,10 @@ interface ModelMapper {
|
||||
fun toV0Instruction(addRecipeInstructionInfo: AddRecipeInstructionInfo): AddRecipeInstructionV0
|
||||
|
||||
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.AddRecipeInstructionInfo
|
||||
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.FullShoppingListInfo
|
||||
import gq.kirmanak.mealient.datasource.models.NewShoppingListItemInfo
|
||||
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLInfo
|
||||
import gq.kirmanak.mealient.datasource.models.RecipeIngredientInfo
|
||||
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.ShoppingListItemRecipeReferenceInfo
|
||||
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.v0.models.AddRecipeIngredientV0
|
||||
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.AddRecipeSettingsV1
|
||||
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.GetRecipeIngredientUnitResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
|
||||
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.GetShoppingListsResponseV1
|
||||
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.UpdateRecipeRequestV1
|
||||
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
|
||||
@@ -198,12 +208,26 @@ class ModelMapperImpl @Inject constructor() : ModelMapper {
|
||||
isFood = getShoppingListItemResponseV1.isFood,
|
||||
note = getShoppingListItemResponseV1.note,
|
||||
quantity = getShoppingListItemResponseV1.quantity,
|
||||
unit = getShoppingListItemResponseV1.unit?.name.orEmpty(),
|
||||
food = getShoppingListItemResponseV1.food?.name.orEmpty(),
|
||||
unit = getShoppingListItemResponseV1.unit?.let { toUnitInfo(it) },
|
||||
food = getShoppingListItemResponseV1.food?.let { toFoodInfo(it) },
|
||||
recipeReferences = getShoppingListItemResponseV1.recipeReferences.map { it.recipeId }
|
||||
.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(
|
||||
getShoppingListItemRecipeReferenceFullResponseV1: GetShoppingListItemRecipeReferenceFullResponseV1
|
||||
) = ShoppingListItemRecipeReferenceInfo(
|
||||
@@ -305,4 +329,39 @@ class ModelMapperImpl @Inject constructor() : ModelMapper {
|
||||
override fun toV0Request(parseRecipeURLInfo: ParseRecipeURLInfo) = ParseRecipeURLRequestV0(
|
||||
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