Implement the dialog to add a new shopping list (#280)
* Add endpoint to create new shopping lists * Initialize editing of lists names * Implement adding new lists * Fix invalid password for demo * Use StateFlow to avoid lost state updates * Refactor the list update to support empty lists * Hide add new list button if there's a new list * Scroll to the newly added list or item * Replace deprecated Divider * Move new field name input to dialog * Display a modal dialog instead of bottom sheet * Reduce unnecessary recompositions * Do not hide button since it is overlapped by dialog * Extract Composable for editable items * Remove unused imports * Add UI for removing and editing shopping lists * Implement editing list name and removing lists * Fix initial cursor state when editing name * Add capitalization of list names * Fix color of divider in dark mode
This commit is contained in:
@@ -7,7 +7,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -73,7 +74,9 @@ private fun IngredientListItem(
|
|||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
|
|
||||||
Divider()
|
HorizontalDivider(
|
||||||
|
color = LocalContentColor.current
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import androidx.compose.foundation.layout.Column
|
|||||||
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.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.Divider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -86,7 +87,9 @@ private fun InstructionListItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (ingredients.isNotEmpty()) {
|
if (ingredients.isNotEmpty()) {
|
||||||
Divider()
|
HorizontalDivider(
|
||||||
|
color = LocalContentColor.current
|
||||||
|
)
|
||||||
ingredients.forEach { ingredient ->
|
ingredients.forEach { ingredient ->
|
||||||
Text(
|
Text(
|
||||||
text = ingredient.display,
|
text = ingredient.display,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<string name="fragment_add_recipe_clear_button">Clear</string>
|
<string name="fragment_add_recipe_clear_button">Clear</string>
|
||||||
<string name="fragment_base_url_url_input_helper_text">Example: demo.mealie.io</string>
|
<string name="fragment_base_url_url_input_helper_text">Example: demo.mealie.io</string>
|
||||||
<string name="fragment_authentication_email_input_helper_text">Example: changeme@example.com</string>
|
<string name="fragment_authentication_email_input_helper_text">Example: changeme@example.com</string>
|
||||||
<string name="fragment_authentication_password_input_helper_text">Example: demo</string>
|
<string name="fragment_authentication_password_input_helper_text">Example: MyPassword</string>
|
||||||
<string name="fragment_recipes_last_page_loaded_toast">Last page loaded</string>
|
<string name="fragment_recipes_last_page_loaded_toast">Last page loaded</string>
|
||||||
<string name="fragment_recipes_load_failure_toast" comment="EXAMPLE: Load error: unauthorized.">Load error: %1$s.</string>
|
<string name="fragment_recipes_load_failure_toast" comment="EXAMPLE: Load error: unauthorized.">Load error: %1$s.</string>
|
||||||
<string name="fragment_recipes_load_failure_toast_no_reason">Load failed.</string>
|
<string name="fragment_recipes_load_failure_toast_no_reason">Load failed.</string>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
|
|||||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||||
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
|
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
|
||||||
@@ -75,4 +76,10 @@ interface MealieDataSource {
|
|||||||
suspend fun getUnits(): GetUnitsResponse
|
suspend fun getUnits(): GetUnitsResponse
|
||||||
|
|
||||||
suspend fun addShoppingListItem(request: CreateShoppingListItemRequest)
|
suspend fun addShoppingListItem(request: CreateShoppingListItemRequest)
|
||||||
|
|
||||||
|
suspend fun addShoppingList(request: CreateShoppingListRequest)
|
||||||
|
|
||||||
|
suspend fun deleteShoppingList(id: String)
|
||||||
|
|
||||||
|
suspend fun updateShoppingListName(id: String, name: String)
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
|
|||||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||||
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetRecipesResponse
|
import gq.kirmanak.mealient.datasource.models.GetRecipesResponse
|
||||||
@@ -61,4 +62,12 @@ internal interface MealieService {
|
|||||||
suspend fun getUnits(perPage: Int): GetUnitsResponse
|
suspend fun getUnits(perPage: Int): GetUnitsResponse
|
||||||
|
|
||||||
suspend fun createShoppingListItem(request: CreateShoppingListItemRequest)
|
suspend fun createShoppingListItem(request: CreateShoppingListItemRequest)
|
||||||
|
|
||||||
|
suspend fun createShoppingList(request: CreateShoppingListRequest)
|
||||||
|
|
||||||
|
suspend fun deleteShoppingList(id: String)
|
||||||
|
|
||||||
|
suspend fun updateShoppingList(id: String, request: JsonElement)
|
||||||
|
|
||||||
|
suspend fun getShoppingListJson(id: String) : JsonElement
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
|
|||||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||||
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.ErrorDetail
|
import gq.kirmanak.mealient.datasource.models.ErrorDetail
|
||||||
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||||
@@ -37,7 +38,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
) : MealieDataSource {
|
) : MealieDataSource {
|
||||||
|
|
||||||
override suspend fun createRecipe(
|
override suspend fun createRecipe(
|
||||||
recipe: CreateRecipeRequest
|
recipe: CreateRecipeRequest,
|
||||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.createRecipe(recipe) },
|
block = { service.createRecipe(recipe) },
|
||||||
logMethod = { "createRecipe" },
|
logMethod = { "createRecipe" },
|
||||||
@@ -46,7 +47,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun updateRecipe(
|
override suspend fun updateRecipe(
|
||||||
slug: String,
|
slug: String,
|
||||||
recipe: UpdateRecipeRequest
|
recipe: UpdateRecipeRequest,
|
||||||
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.updateRecipe(recipe, slug) },
|
block = { service.updateRecipe(recipe, slug) },
|
||||||
logMethod = { "updateRecipe" },
|
logMethod = { "updateRecipe" },
|
||||||
@@ -80,7 +81,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun requestRecipes(
|
override suspend fun requestRecipes(
|
||||||
page: Int,
|
page: Int,
|
||||||
perPage: Int
|
perPage: Int,
|
||||||
): List<GetRecipeSummaryResponse> = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): List<GetRecipeSummaryResponse> = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.getRecipeSummary(page, perPage) },
|
block = { service.getRecipeSummary(page, perPage) },
|
||||||
logMethod = { "requestRecipes" },
|
logMethod = { "requestRecipes" },
|
||||||
@@ -88,7 +89,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
).items
|
).items
|
||||||
|
|
||||||
override suspend fun requestRecipeInfo(
|
override suspend fun requestRecipeInfo(
|
||||||
slug: String
|
slug: String,
|
||||||
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.getRecipe(slug) },
|
block = { service.getRecipe(slug) },
|
||||||
logMethod = { "requestRecipeInfo" },
|
logMethod = { "requestRecipeInfo" },
|
||||||
@@ -96,7 +97,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun parseRecipeFromURL(
|
override suspend fun parseRecipeFromURL(
|
||||||
request: ParseRecipeURLRequest
|
request: ParseRecipeURLRequest,
|
||||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.createRecipeFromURL(request) },
|
block = { service.createRecipeFromURL(request) },
|
||||||
logMethod = { "parseRecipeFromURL" },
|
logMethod = { "parseRecipeFromURL" },
|
||||||
@@ -104,7 +105,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun createApiToken(
|
override suspend fun createApiToken(
|
||||||
request: CreateApiTokenRequest
|
request: CreateApiTokenRequest,
|
||||||
): CreateApiTokenResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): CreateApiTokenResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.createApiToken(request) },
|
block = { service.createApiToken(request) },
|
||||||
logMethod = { "createApiToken" },
|
logMethod = { "createApiToken" },
|
||||||
@@ -120,7 +121,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun removeFavoriteRecipe(
|
override suspend fun removeFavoriteRecipe(
|
||||||
userId: String,
|
userId: String,
|
||||||
recipeSlug: String
|
recipeSlug: String,
|
||||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.removeFavoriteRecipe(userId, recipeSlug) },
|
block = { service.removeFavoriteRecipe(userId, recipeSlug) },
|
||||||
logMethod = { "removeFavoriteRecipe" },
|
logMethod = { "removeFavoriteRecipe" },
|
||||||
@@ -129,7 +130,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun addFavoriteRecipe(
|
override suspend fun addFavoriteRecipe(
|
||||||
userId: String,
|
userId: String,
|
||||||
recipeSlug: String
|
recipeSlug: String,
|
||||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.addFavoriteRecipe(userId, recipeSlug) },
|
block = { service.addFavoriteRecipe(userId, recipeSlug) },
|
||||||
logMethod = { "addFavoriteRecipe" },
|
logMethod = { "addFavoriteRecipe" },
|
||||||
@@ -137,7 +138,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun deleteRecipe(
|
override suspend fun deleteRecipe(
|
||||||
slug: String
|
slug: String,
|
||||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.deleteRecipe(slug) },
|
block = { service.deleteRecipe(slug) },
|
||||||
logMethod = { "deleteRecipe" },
|
logMethod = { "deleteRecipe" },
|
||||||
@@ -154,7 +155,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun getShoppingList(
|
override suspend fun getShoppingList(
|
||||||
id: String
|
id: String,
|
||||||
): GetShoppingListResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
): GetShoppingListResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.getShoppingList(id) },
|
block = { service.getShoppingList(id) },
|
||||||
logMethod = { "getShoppingList" },
|
logMethod = { "getShoppingList" },
|
||||||
@@ -187,7 +188,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun updateShoppingListItem(
|
override suspend fun updateShoppingListItem(
|
||||||
item: GetShoppingListItemResponse
|
item: GetShoppingListItemResponse,
|
||||||
) {
|
) {
|
||||||
// Has to be done in two steps because we can't specify only the changed fields
|
// Has to be done in two steps because we can't specify only the changed fields
|
||||||
val remoteItem = getShoppingListItem(item.id)
|
val remoteItem = getShoppingListItem(item.id)
|
||||||
@@ -219,10 +220,55 @@ internal class MealieDataSourceImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun addShoppingListItem(
|
override suspend fun addShoppingListItem(
|
||||||
request: CreateShoppingListItemRequest
|
request: CreateShoppingListItemRequest,
|
||||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
block = { service.createShoppingListItem(request) },
|
block = { service.createShoppingListItem(request) },
|
||||||
logMethod = { "addShoppingListItem" },
|
logMethod = { "addShoppingListItem" },
|
||||||
logParameters = { "request = $request" }
|
logParameters = { "request = $request" }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override suspend fun addShoppingList(
|
||||||
|
request: CreateShoppingListRequest,
|
||||||
|
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.createShoppingList(request) },
|
||||||
|
logMethod = { "createShoppingList" },
|
||||||
|
logParameters = { "request = $request" }
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun updateShoppingList(
|
||||||
|
id: String,
|
||||||
|
request: JsonElement,
|
||||||
|
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.updateShoppingList(id, request) },
|
||||||
|
logMethod = { "updateShoppingList" },
|
||||||
|
logParameters = { "id = $id, request = $request" }
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun getShoppingListJson(
|
||||||
|
id: String,
|
||||||
|
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.getShoppingListJson(id) },
|
||||||
|
logMethod = { "getShoppingListJson" },
|
||||||
|
logParameters = { "id = $id" }
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun deleteShoppingList(
|
||||||
|
id: String,
|
||||||
|
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.deleteShoppingList(id) },
|
||||||
|
logMethod = { "deleteShoppingList" },
|
||||||
|
logParameters = { "id = $id" }
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun updateShoppingListName(
|
||||||
|
id: String,
|
||||||
|
name: String
|
||||||
|
) {
|
||||||
|
// Has to be done in two steps because we can't specify only the changed fields
|
||||||
|
val remoteItem = getShoppingListJson(id)
|
||||||
|
val updatedItem = remoteItem.jsonObject.toMutableMap().apply {
|
||||||
|
put("name", JsonPrimitive(name))
|
||||||
|
}.let(::JsonObject)
|
||||||
|
updateShoppingList(id, updatedItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
|
|||||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||||
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetRecipesResponse
|
import gq.kirmanak.mealient.datasource.models.GetRecipesResponse
|
||||||
@@ -196,6 +197,34 @@ internal class MealieServiceKtor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun createShoppingList(request: CreateShoppingListRequest) {
|
||||||
|
httpClient.post {
|
||||||
|
endpoint("/api/groups/shopping/lists")
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteShoppingList(id: String) {
|
||||||
|
httpClient.delete {
|
||||||
|
endpoint("/api/groups/shopping/lists/$id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateShoppingList(id: String, request: JsonElement) {
|
||||||
|
httpClient.put {
|
||||||
|
endpoint("/api/groups/shopping/lists/$id")
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getShoppingListJson(id: String): JsonElement {
|
||||||
|
return httpClient.get {
|
||||||
|
endpoint("/api/groups/shopping/lists/$id")
|
||||||
|
}.body()
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun HttpRequestBuilder.endpoint(
|
private suspend fun HttpRequestBuilder.endpoint(
|
||||||
path: String,
|
path: String,
|
||||||
block: URLBuilder.() -> Unit = {},
|
block: URLBuilder.() -> Unit = {},
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package gq.kirmanak.mealient.datasource.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateShoppingListRequest(
|
||||||
|
@SerialName("name") val name: String,
|
||||||
|
)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.network
|
package gq.kirmanak.mealient.shopping_lists.network
|
||||||
|
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||||
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
|
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||||
@@ -22,4 +23,10 @@ interface ShoppingListsDataSource {
|
|||||||
suspend fun getUnits(): List<GetUnitResponse>
|
suspend fun getUnits(): List<GetUnitResponse>
|
||||||
|
|
||||||
suspend fun addShoppingListItem(item: CreateShoppingListItemRequest)
|
suspend fun addShoppingListItem(item: CreateShoppingListItemRequest)
|
||||||
|
|
||||||
|
suspend fun addShoppingList(request: CreateShoppingListRequest)
|
||||||
|
|
||||||
|
suspend fun deleteShoppingList(id: String)
|
||||||
|
|
||||||
|
suspend fun updateShoppingListName(id: String, name: String)
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package gq.kirmanak.mealient.shopping_lists.network
|
|||||||
|
|
||||||
import gq.kirmanak.mealient.datasource.MealieDataSource
|
import gq.kirmanak.mealient.datasource.MealieDataSource
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||||
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
|
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||||
@@ -27,7 +28,7 @@ class ShoppingListsDataSourceImpl @Inject constructor(
|
|||||||
) = dataSource.deleteShoppingListItem(id)
|
) = dataSource.deleteShoppingListItem(id)
|
||||||
|
|
||||||
override suspend fun updateShoppingListItem(
|
override suspend fun updateShoppingListItem(
|
||||||
item: GetShoppingListItemResponse
|
item: GetShoppingListItemResponse,
|
||||||
) = dataSource.updateShoppingListItem(item)
|
) = dataSource.updateShoppingListItem(item)
|
||||||
|
|
||||||
override suspend fun getFoods(): List<GetFoodResponse> = dataSource.getFoods().items
|
override suspend fun getFoods(): List<GetFoodResponse> = dataSource.getFoods().items
|
||||||
@@ -35,7 +36,18 @@ class ShoppingListsDataSourceImpl @Inject constructor(
|
|||||||
override suspend fun getUnits(): List<GetUnitResponse> = dataSource.getUnits().items
|
override suspend fun getUnits(): List<GetUnitResponse> = dataSource.getUnits().items
|
||||||
|
|
||||||
override suspend fun addShoppingListItem(
|
override suspend fun addShoppingListItem(
|
||||||
item: CreateShoppingListItemRequest
|
item: CreateShoppingListItemRequest,
|
||||||
) = dataSource.addShoppingListItem(item)
|
) = dataSource.addShoppingListItem(item)
|
||||||
|
|
||||||
|
override suspend fun addShoppingList(
|
||||||
|
request: CreateShoppingListRequest,
|
||||||
|
) = dataSource.addShoppingList(request)
|
||||||
|
|
||||||
|
override suspend fun updateShoppingListName(
|
||||||
|
id: String,
|
||||||
|
name: String
|
||||||
|
) = dataSource.updateShoppingListName(id, name)
|
||||||
|
|
||||||
|
override suspend fun deleteShoppingList(id: String) = dataSource.deleteShoppingList(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.repo
|
package gq.kirmanak.mealient.shopping_lists.repo
|
||||||
|
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||||
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
|
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||||
@@ -22,4 +23,10 @@ interface ShoppingListsRepo {
|
|||||||
suspend fun getUnits(): List<GetUnitResponse>
|
suspend fun getUnits(): List<GetUnitResponse>
|
||||||
|
|
||||||
suspend fun addShoppingListItem(item: CreateShoppingListItemRequest)
|
suspend fun addShoppingListItem(item: CreateShoppingListItemRequest)
|
||||||
|
|
||||||
|
suspend fun addShoppingList(request: CreateShoppingListRequest)
|
||||||
|
|
||||||
|
suspend fun deleteShoppingList(id: String)
|
||||||
|
|
||||||
|
suspend fun updateShoppingListName(id: String, name: String)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.repo
|
package gq.kirmanak.mealient.shopping_lists.repo
|
||||||
|
|
||||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
|
||||||
|
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
|
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||||
@@ -49,4 +50,20 @@ class ShoppingListsRepoImpl @Inject constructor(
|
|||||||
logger.v { "addShoppingListItem() called with: item = $item" }
|
logger.v { "addShoppingListItem() called with: item = $item" }
|
||||||
dataSource.addShoppingListItem(item)
|
dataSource.addShoppingListItem(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun addShoppingList(request: CreateShoppingListRequest) {
|
||||||
|
logger.v { "addShoppingList() called with: request = $request" }
|
||||||
|
dataSource.addShoppingList(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateShoppingListName(id: String, name: String) {
|
||||||
|
logger.v { "updateShoppingListName() called with: id = $id, name = $name" }
|
||||||
|
dataSource.updateShoppingListName(id, name)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteShoppingList(id: String) {
|
||||||
|
logger.v { "deleteShoppingList() called with: id = $id" }
|
||||||
|
dataSource.deleteShoppingList(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package gq.kirmanak.mealient.shopping_lists.ui.composables
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SwipeToDismissBox
|
||||||
|
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import gq.kirmanak.mealient.ui.Dimens
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
internal fun EditableItemBox(
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onEdit: () -> Unit,
|
||||||
|
deleteContentDescription: String,
|
||||||
|
editContentDescription: String,
|
||||||
|
content: @Composable (RowScope.() -> Unit),
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val dismissState = rememberSwipeToDismissBoxState()
|
||||||
|
|
||||||
|
LaunchedEffect(dismissState.currentValue, onDelete, onEdit) {
|
||||||
|
when (dismissState.currentValue) {
|
||||||
|
SwipeToDismissBoxValue.EndToStart -> {
|
||||||
|
onDelete()
|
||||||
|
dismissState.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
SwipeToDismissBoxValue.StartToEnd -> {
|
||||||
|
onEdit()
|
||||||
|
dismissState.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
SwipeToDismissBoxValue.Settled -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SwipeToDismissBox(
|
||||||
|
modifier = modifier,
|
||||||
|
state = dismissState,
|
||||||
|
content = content,
|
||||||
|
backgroundContent = {
|
||||||
|
if (dismissState.targetValue == SwipeToDismissBoxValue.EndToStart) {
|
||||||
|
EditableItemBoxBackground(
|
||||||
|
icon = Icons.Default.Delete,
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.error,
|
||||||
|
iconAlignment = Alignment.CenterEnd,
|
||||||
|
contentDescription = deleteContentDescription
|
||||||
|
)
|
||||||
|
} else if (dismissState.targetValue == SwipeToDismissBoxValue.StartToEnd) {
|
||||||
|
EditableItemBoxBackground(
|
||||||
|
icon = Icons.Default.Edit,
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.primary,
|
||||||
|
iconAlignment = Alignment.CenterStart,
|
||||||
|
contentDescription = editContentDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EditableItemBoxBackground(
|
||||||
|
icon: ImageVector,
|
||||||
|
backgroundColor: Color,
|
||||||
|
iconAlignment: Alignment,
|
||||||
|
contentDescription: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentColor: Color = MaterialTheme.colorScheme.contentColorFor(backgroundColor)
|
||||||
|
) {
|
||||||
|
val color by animateColorAsState(backgroundColor, label = "background-color")
|
||||||
|
val iconColor by animateColorAsState(contentColor, label = "icon-color")
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(color)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(iconAlignment)
|
||||||
|
.padding(horizontal = Dimens.Small),
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = iconColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package gq.kirmanak.mealient.shopping_lists.ui.composables
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps Material [TextField] and sets initial selection ([TextRange]) to the end of the text.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun MealientTextField(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (newValue: String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
singleLine: Boolean = true,
|
||||||
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
placeholder: @Composable (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
var textFieldValueState by remember {
|
||||||
|
mutableStateOf(TextFieldValue(text = value, selection = TextRange(value.length)))
|
||||||
|
}
|
||||||
|
val textFieldValue = textFieldValueState.copy(text = value)
|
||||||
|
|
||||||
|
SideEffect {
|
||||||
|
if (textFieldValue.selection != textFieldValueState.selection ||
|
||||||
|
textFieldValue.composition != textFieldValueState.composition
|
||||||
|
) {
|
||||||
|
textFieldValueState = textFieldValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastTextValue by remember(value) { mutableStateOf(value) }
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
modifier = modifier,
|
||||||
|
value = textFieldValueState,
|
||||||
|
onValueChange = { newTextFieldValueState ->
|
||||||
|
textFieldValueState = newTextFieldValueState
|
||||||
|
|
||||||
|
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
|
||||||
|
lastTextValue = newTextFieldValueState.text
|
||||||
|
|
||||||
|
if (stringChangedSinceLastInvocation) {
|
||||||
|
onValueChange((newTextFieldValueState.text))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
placeholder = placeholder,
|
||||||
|
singleLine = singleLine,
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
trailingIcon = trailingIcon
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,23 +1,18 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.ui.details
|
package gq.kirmanak.mealient.shopping_lists.ui.details
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
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.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
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.Add
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Close
|
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.NoMeals
|
||||||
import androidx.compose.material.icons.filled.Restaurant
|
import androidx.compose.material.icons.filled.Restaurant
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
@@ -31,15 +26,12 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.SwipeToDismissBox
|
|
||||||
import androidx.compose.material3.SwipeToDismissBoxState
|
|
||||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -56,6 +48,7 @@ import gq.kirmanak.mealient.datasource.models.GetShoppingListItemRecipeReference
|
|||||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
||||||
import gq.kirmanak.mealient.datasource.models.GetUnitResponse
|
import gq.kirmanak.mealient.datasource.models.GetUnitResponse
|
||||||
import gq.kirmanak.mealient.shopping_list.R
|
import gq.kirmanak.mealient.shopping_list.R
|
||||||
|
import gq.kirmanak.mealient.shopping_lists.ui.composables.EditableItemBox
|
||||||
import gq.kirmanak.mealient.shopping_lists.ui.composables.getErrorMessage
|
import gq.kirmanak.mealient.shopping_lists.ui.composables.getErrorMessage
|
||||||
import gq.kirmanak.mealient.ui.AppTheme
|
import gq.kirmanak.mealient.ui.AppTheme
|
||||||
import gq.kirmanak.mealient.ui.Dimens
|
import gq.kirmanak.mealient.ui.Dimens
|
||||||
@@ -101,7 +94,6 @@ internal fun ShoppingListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ShoppingListScreen(
|
private fun ShoppingListScreen(
|
||||||
loadingState: LoadingState<ShoppingListScreenState>,
|
loadingState: LoadingState<ShoppingListScreenState>,
|
||||||
@@ -123,6 +115,12 @@ private fun ShoppingListScreen(
|
|||||||
loadingState.data?.name.orEmpty()
|
loadingState.data?.name.orEmpty()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var lastAddedItemIndex by remember { mutableIntStateOf(-1) }
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
LaunchedEffect(lastAddedItemIndex) {
|
||||||
|
if (lastAddedItemIndex >= 0) lazyListState.animateScrollToItem(lastAddedItemIndex)
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumnWithLoadingState(
|
LazyColumnWithLoadingState(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
loadingState = loadingState.map { it.items },
|
loadingState = loadingState.map { it.items },
|
||||||
@@ -145,9 +143,12 @@ private fun ShoppingListScreen(
|
|||||||
contentDescription = stringResource(id = R.string.shopping_list_screen_add_icon_content_description),
|
contentDescription = stringResource(id = R.string.shopping_list_screen_add_icon_content_description),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
lazyListState = lazyListState
|
||||||
) { items ->
|
) { items ->
|
||||||
val firstCheckedItemIndex = items.indexOfFirst { it.checked }
|
val firstCheckedItemIndex = items.indexOfFirst { it.checked }
|
||||||
|
lastAddedItemIndex =
|
||||||
|
items.indexOfLast { it is ShoppingListItemState.NewItem }
|
||||||
|
|
||||||
itemsIndexed(items, { _, item -> item.id }) { index, itemState ->
|
itemsIndexed(items, { _, item -> item.id }) { index, itemState ->
|
||||||
if (itemState is ShoppingListItemState.ExistingItem) {
|
if (itemState is ShoppingListItemState.ExistingItem) {
|
||||||
@@ -507,7 +508,6 @@ fun ShoppingListItemEditorNonFoodPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShoppingListItem(
|
fun ShoppingListItem(
|
||||||
itemState: ShoppingListItemState.ExistingItem,
|
itemState: ShoppingListItemState.ExistingItem,
|
||||||
@@ -516,57 +516,13 @@ fun ShoppingListItem(
|
|||||||
onCheckedChange: (Boolean) -> Unit = {},
|
onCheckedChange: (Boolean) -> Unit = {},
|
||||||
onDismissed: () -> Unit = {},
|
onDismissed: () -> Unit = {},
|
||||||
onEditStart: () -> Unit = {},
|
onEditStart: () -> Unit = {},
|
||||||
dismissState: SwipeToDismissBoxState = rememberSwipeToDismissBoxState(
|
|
||||||
confirmValueChange = {
|
|
||||||
when (it) {
|
|
||||||
SwipeToDismissBoxValue.EndToStart -> onDismissed()
|
|
||||||
SwipeToDismissBoxValue.StartToEnd -> onEditStart()
|
|
||||||
SwipeToDismissBoxValue.Settled -> Unit
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
val shoppingListItem = itemState.item
|
EditableItemBox(
|
||||||
SwipeToDismissBox(
|
modifier = modifier,
|
||||||
state = dismissState,
|
onDelete = onDismissed,
|
||||||
backgroundContent = {
|
onEdit = onEditStart,
|
||||||
if (dismissState.targetValue == SwipeToDismissBoxValue.EndToStart) {
|
deleteContentDescription = stringResource(R.string.shopping_list_screen_delete_icon_content_description),
|
||||||
val color by animateColorAsState(MaterialTheme.colorScheme.error)
|
editContentDescription = stringResource(R.string.shopping_list_screen_edit_icon_content_description),
|
||||||
val iconColor by animateColorAsState(MaterialTheme.colorScheme.onError)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(color)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Delete,
|
|
||||||
contentDescription = stringResource(R.string.shopping_list_screen_delete_icon_content_description),
|
|
||||||
tint = iconColor,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterEnd)
|
|
||||||
.padding(end = Dimens.Small)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (dismissState.targetValue == SwipeToDismissBoxValue.StartToEnd) {
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
content = {
|
content = {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -585,6 +541,7 @@ fun ShoppingListItem(
|
|||||||
onCheckedChange = onCheckedChange,
|
onCheckedChange = onCheckedChange,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val shoppingListItem = itemState.item
|
||||||
val isFood = shoppingListItem.isFood
|
val isFood = shoppingListItem.isFood
|
||||||
val quantity = shoppingListItem.quantity
|
val quantity = shoppingListItem.quantity
|
||||||
.takeUnless { it == 0.0 }
|
.takeUnless { it == 0.0 }
|
||||||
@@ -601,11 +558,9 @@ fun ShoppingListItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = modifier,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
@ColorSchemePreview
|
@ColorSchemePreview
|
||||||
fun PreviewShoppingListItemChecked() {
|
fun PreviewShoppingListItemChecked() {
|
||||||
@@ -617,7 +572,6 @@ fun PreviewShoppingListItemChecked() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
@ColorSchemePreview
|
@ColorSchemePreview
|
||||||
fun PreviewShoppingListItemUnchecked() {
|
fun PreviewShoppingListItemUnchecked() {
|
||||||
@@ -629,36 +583,6 @@ fun PreviewShoppingListItemUnchecked() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
@ColorSchemePreview
|
|
||||||
fun PreviewShoppingListItemDismissed() {
|
|
||||||
AppTheme {
|
|
||||||
ShoppingListItem(
|
|
||||||
itemState = ShoppingListItemState.ExistingItem(PreviewData.blackTeaBags),
|
|
||||||
showDivider = false,
|
|
||||||
dismissState = rememberSwipeToDismissBoxState(
|
|
||||||
initialValue = SwipeToDismissBoxValue.EndToStart,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
@ColorSchemePreview
|
|
||||||
fun PreviewShoppingListItemEditing() {
|
|
||||||
AppTheme {
|
|
||||||
ShoppingListItem(
|
|
||||||
itemState = ShoppingListItemState.ExistingItem(PreviewData.blackTeaBags),
|
|
||||||
showDivider = false,
|
|
||||||
dismissState = rememberSwipeToDismissBoxState(
|
|
||||||
initialValue = SwipeToDismissBoxValue.StartToEnd,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private object PreviewData {
|
private object PreviewData {
|
||||||
|
|
||||||
val blackTeaBags = GetShoppingListItemResponse(
|
val blackTeaBags = GetShoppingListItemResponse(
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package gq.kirmanak.mealient.shopping_lists.ui.list
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import gq.kirmanak.mealient.shopping_list.R
|
||||||
|
import gq.kirmanak.mealient.ui.AppTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun DeleteListConfirmDialog(
|
||||||
|
onEvent: (ShoppingListsEvent) -> Unit,
|
||||||
|
onConfirm: ShoppingListsEvent,
|
||||||
|
listName: String,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
onEvent(ShoppingListsEvent.DialogDismissed)
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { onEvent(ShoppingListsEvent.DialogDismissed) }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Cancel,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_lists_dialog_cancel)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { onEvent(onConfirm) },
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_lists_dialog_confirm)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.shopping_lists_dialog_delete_confirm_title,
|
||||||
|
listName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.shopping_lists_dialog_delete_confirm_text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun DeleteListConfirmDialogPreview() {
|
||||||
|
AppTheme {
|
||||||
|
DeleteListConfirmDialog(
|
||||||
|
onEvent = {},
|
||||||
|
onConfirm = ShoppingListsEvent.DialogDismissed,
|
||||||
|
listName = "Test"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package gq.kirmanak.mealient.shopping_lists.ui.list
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardActionScope
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Clear
|
||||||
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import gq.kirmanak.mealient.shopping_list.R
|
||||||
|
import gq.kirmanak.mealient.shopping_lists.ui.composables.MealientTextField
|
||||||
|
import gq.kirmanak.mealient.ui.AppTheme
|
||||||
|
import gq.kirmanak.mealient.ui.Dimens
|
||||||
|
import gq.kirmanak.mealient.ui.preview.ColorSchemePreview
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
internal fun ShoppingListNameDialog(
|
||||||
|
onEvent: (ShoppingListsEvent) -> Unit,
|
||||||
|
listName: String,
|
||||||
|
onConfirm: ShoppingListsEvent,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
oldName: String? = null
|
||||||
|
) {
|
||||||
|
BasicAlertDialog(
|
||||||
|
modifier = modifier,
|
||||||
|
onDismissRequest = {
|
||||||
|
onEvent(ShoppingListsEvent.DialogDismissed)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
ShoppingListNameDialogContent(
|
||||||
|
listName = listName,
|
||||||
|
onEvent = onEvent,
|
||||||
|
oldName = oldName,
|
||||||
|
onConfirm = onConfirm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShoppingListNameDialogContent(
|
||||||
|
listName: String,
|
||||||
|
onEvent: (ShoppingListsEvent) -> Unit,
|
||||||
|
oldName: String?,
|
||||||
|
onConfirm: ShoppingListsEvent,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(Dimens.Small),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(Dimens.Medium),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimens.Medium)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (oldName == null) {
|
||||||
|
stringResource(R.string.shopping_lists_dialog_add_new_title)
|
||||||
|
} else {
|
||||||
|
stringResource(id = R.string.shopping_lists_dialog_edit_name_title, oldName)
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
NameTextField(
|
||||||
|
listName = listName,
|
||||||
|
isEdit = oldName != null,
|
||||||
|
onEvent = onEvent
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.Small, Alignment.End)
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { onEvent(ShoppingListsEvent.DialogDismissed) }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Cancel,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_lists_dialog_cancel)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (oldName == null) {
|
||||||
|
onEvent(onConfirm)
|
||||||
|
} else {
|
||||||
|
onEvent(onConfirm)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = listName.isNotBlank()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_lists_dialog_confirm)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NameTextField(
|
||||||
|
listName: String,
|
||||||
|
isEdit: Boolean,
|
||||||
|
onEvent: (ShoppingListsEvent) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val onDone: KeyboardActionScope.() -> Unit = {
|
||||||
|
onEvent(ShoppingListsEvent.NewListSaved(listName))
|
||||||
|
}
|
||||||
|
|
||||||
|
val onEdit: (newValue: String) -> Unit = { newValue ->
|
||||||
|
if (isEdit) {
|
||||||
|
onEvent(ShoppingListsEvent.EditListInput(newValue))
|
||||||
|
} else {
|
||||||
|
onEvent(ShoppingListsEvent.NewListInput(newValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val trailingIcon: @Composable () -> Unit = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { onEdit("") }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Clear,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_lists_dialog_name_clear_input)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isInputEmpty = listName.isBlank()
|
||||||
|
|
||||||
|
MealientTextField(
|
||||||
|
modifier = modifier
|
||||||
|
.showKeyboard()
|
||||||
|
.fillMaxWidth(),
|
||||||
|
value = listName,
|
||||||
|
onValueChange = { onEdit(it) },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.shopping_lists_screen_list_name_placeholder)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
imeAction = if (isInputEmpty) ImeAction.None else ImeAction.Done,
|
||||||
|
capitalization = KeyboardCapitalization.Sentences
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = onDone.takeUnless { isInputEmpty },
|
||||||
|
),
|
||||||
|
trailingIcon = trailingIcon.takeUnless { isInputEmpty }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Modifier.showKeyboard() = composed {
|
||||||
|
val windowInfo = LocalWindowInfo.current
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
LaunchedEffect(windowInfo) {
|
||||||
|
snapshotFlow { windowInfo.isWindowFocused }.collect { isWindowFocused ->
|
||||||
|
if (isWindowFocused) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focusRequester(focusRequester)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorSchemePreview
|
||||||
|
@Composable
|
||||||
|
private fun ShoppingListNameDialogEditPreview() {
|
||||||
|
AppTheme {
|
||||||
|
ShoppingListNameDialog(
|
||||||
|
listName = "Test",
|
||||||
|
onConfirm = ShoppingListsEvent.DialogDismissed,
|
||||||
|
oldName = "Old test",
|
||||||
|
onEvent = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorSchemePreview
|
||||||
|
@Composable
|
||||||
|
private fun ShoppingListNameDialogNewPreview() {
|
||||||
|
AppTheme {
|
||||||
|
ShoppingListNameDialog(
|
||||||
|
listName = "Test",
|
||||||
|
onConfirm = ShoppingListsEvent.DialogDismissed,
|
||||||
|
onEvent = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.ui.list
|
package gq.kirmanak.mealient.shopping_lists.ui.list
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
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.ShoppingCart
|
import androidx.compose.material.icons.filled.ShoppingCart
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -21,8 +24,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.navigation.navigate
|
import com.ramcosta.composedestinations.navigation.navigate
|
||||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListsSummaryResponse
|
|
||||||
import gq.kirmanak.mealient.shopping_list.R
|
import gq.kirmanak.mealient.shopping_list.R
|
||||||
|
import gq.kirmanak.mealient.shopping_lists.ui.composables.EditableItemBox
|
||||||
import gq.kirmanak.mealient.shopping_lists.ui.composables.getErrorMessage
|
import gq.kirmanak.mealient.shopping_lists.ui.composables.getErrorMessage
|
||||||
import gq.kirmanak.mealient.shopping_lists.ui.destinations.ShoppingListScreenDestination
|
import gq.kirmanak.mealient.shopping_lists.ui.destinations.ShoppingListScreenDestination
|
||||||
import gq.kirmanak.mealient.ui.AppTheme
|
import gq.kirmanak.mealient.ui.AppTheme
|
||||||
@@ -35,33 +38,57 @@ import gq.kirmanak.mealient.ui.util.error
|
|||||||
|
|
||||||
@Destination
|
@Destination
|
||||||
@Composable
|
@Composable
|
||||||
fun ShoppingListsScreen(
|
internal fun ShoppingListsScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
baseScreenState: BaseScreenState,
|
baseScreenState: BaseScreenState,
|
||||||
shoppingListsViewModel: ShoppingListsViewModel = hiltViewModel(),
|
shoppingListsViewModel: ShoppingListsViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val loadingState by shoppingListsViewModel.loadingState.collectAsState()
|
val screenState by shoppingListsViewModel.shoppingListsState.collectAsState()
|
||||||
val errorToShowInSnackbar = shoppingListsViewModel.errorToShowInSnackBar
|
|
||||||
|
ShoppingListsScreenDialog(
|
||||||
|
dialog = screenState.dialog,
|
||||||
|
onEvent = shoppingListsViewModel::onEvent
|
||||||
|
)
|
||||||
|
|
||||||
BaseScreenWithNavigation(
|
BaseScreenWithNavigation(
|
||||||
baseScreenState = baseScreenState,
|
baseScreenState = baseScreenState,
|
||||||
) { modifier ->
|
) { modifier ->
|
||||||
LazyColumnWithLoadingState(
|
LazyColumnWithLoadingState(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
loadingState = loadingState,
|
loadingState = screenState.loadingState,
|
||||||
emptyListError = loadingState.error?.let { getErrorMessage(it) }
|
emptyListError = screenState.loadingState.error?.let { getErrorMessage(it) }
|
||||||
?: stringResource(R.string.shopping_lists_screen_empty),
|
?: stringResource(R.string.shopping_lists_screen_empty),
|
||||||
retryButtonText = stringResource(id = R.string.shopping_lists_screen_empty_button_refresh),
|
retryButtonText = stringResource(id = R.string.shopping_lists_screen_empty_button_refresh),
|
||||||
snackbarText = errorToShowInSnackbar?.let { getErrorMessage(error = it) },
|
snackbarText = screenState.errorToShow?.let { getErrorMessage(error = it) },
|
||||||
onSnackbarShown = shoppingListsViewModel::onSnackbarShown,
|
onSnackbarShown = { shoppingListsViewModel.onEvent(ShoppingListsEvent.SnackbarShown) },
|
||||||
onRefresh = shoppingListsViewModel::refresh
|
onRefresh = { shoppingListsViewModel.onEvent(ShoppingListsEvent.RefreshRequested) },
|
||||||
|
floatingActionButton = {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = { shoppingListsViewModel.onEvent(ShoppingListsEvent.AddShoppingList) }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_lists_screen_add_icon_content_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
) { items ->
|
) { items ->
|
||||||
items(items) { shoppingList ->
|
items(
|
||||||
|
items = items,
|
||||||
|
key = { it.id },
|
||||||
|
contentType = { "Existing list" }
|
||||||
|
) { displayList ->
|
||||||
ShoppingListCard(
|
ShoppingListCard(
|
||||||
shoppingList = shoppingList,
|
listName = displayList.name,
|
||||||
onItemClick = { clickedEntity ->
|
onClick = {
|
||||||
val shoppingListId = clickedEntity.id
|
val shoppingListId = displayList.id
|
||||||
navController.navigate(ShoppingListScreenDestination(shoppingListId))
|
navController.navigate(ShoppingListScreenDestination(shoppingListId))
|
||||||
|
},
|
||||||
|
onDelete = {
|
||||||
|
shoppingListsViewModel.onEvent(ShoppingListsEvent.RemoveList(displayList))
|
||||||
|
},
|
||||||
|
onEdit = {
|
||||||
|
shoppingListsViewModel.onEvent(ShoppingListsEvent.EditList(displayList))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -70,41 +97,122 @@ fun ShoppingListsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ColorSchemePreview
|
private fun ShoppingListsScreenDialog(
|
||||||
private fun PreviewShoppingListCard() {
|
dialog: ShoppingListsDialog,
|
||||||
AppTheme {
|
onEvent: (ShoppingListsEvent) -> Unit,
|
||||||
ShoppingListCard(
|
) {
|
||||||
shoppingList = GetShoppingListsSummaryResponse("1", "Weekend shopping"),
|
when (dialog) {
|
||||||
)
|
is ShoppingListsDialog.EditListItem -> {
|
||||||
|
ShoppingListNameDialog(
|
||||||
|
onEvent = onEvent,
|
||||||
|
onConfirm = dialog.onConfirm,
|
||||||
|
listName = dialog.listName,
|
||||||
|
oldName = dialog.oldListName
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsDialog.NewListItem -> {
|
||||||
|
ShoppingListNameDialog(
|
||||||
|
onEvent = onEvent,
|
||||||
|
onConfirm = ShoppingListsEvent.NewListSaved(dialog.listName),
|
||||||
|
listName = dialog.listName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
is ShoppingListsDialog.RemoveListItem -> {
|
||||||
|
DeleteListConfirmDialog(
|
||||||
|
onEvent = onEvent,
|
||||||
|
onConfirm = dialog.onConfirm,
|
||||||
|
listName = dialog.listName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsDialog.None -> {
|
||||||
|
Unit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ShoppingListCard(
|
private fun ShoppingListCard(
|
||||||
shoppingList: GetShoppingListsSummaryResponse?,
|
listName: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onEdit: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onItemClick: (GetShoppingListsSummaryResponse) -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
Card(
|
EditableItemBox(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.padding(horizontal = Dimens.Medium, vertical = Dimens.Small)
|
onDelete = onDelete,
|
||||||
.fillMaxWidth()
|
onEdit = onEdit,
|
||||||
.clickable { shoppingList?.let { onItemClick(it) } },
|
deleteContentDescription = stringResource(
|
||||||
) {
|
id = R.string.shopping_list_screen_delete_icon_content_description,
|
||||||
Row(
|
listName
|
||||||
modifier = Modifier.padding(Dimens.Medium),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
editContentDescription = stringResource(
|
||||||
) {
|
id = R.string.shopping_list_screen_edit_icon_content_description,
|
||||||
Icon(
|
listName
|
||||||
imageVector = Icons.Default.ShoppingCart,
|
),
|
||||||
contentDescription = stringResource(id = R.string.shopping_lists_screen_cart_icon),
|
content = {
|
||||||
modifier = Modifier.height(Dimens.Large),
|
Card(
|
||||||
)
|
modifier = Modifier
|
||||||
Text(
|
.padding(
|
||||||
text = shoppingList?.name.orEmpty(),
|
horizontal = Dimens.Medium,
|
||||||
modifier = Modifier.padding(start = Dimens.Medium),
|
vertical = Dimens.Small
|
||||||
)
|
)
|
||||||
}
|
.fillMaxWidth()
|
||||||
|
.clickable(
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(Dimens.Medium),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.Medium)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(Dimens.Large),
|
||||||
|
imageVector = Icons.Default.ShoppingCart,
|
||||||
|
contentDescription = stringResource(id = R.string.shopping_lists_screen_cart_icon),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = listName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ColorSchemePreview
|
||||||
|
private fun PreviewShoppingListCard() {
|
||||||
|
AppTheme {
|
||||||
|
ShoppingListCard(
|
||||||
|
listName = "Weekend shopping",
|
||||||
|
onClick = {},
|
||||||
|
onDelete = {},
|
||||||
|
onEdit = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ColorSchemePreview
|
||||||
|
private fun PreviewEditingShoppingListCard() {
|
||||||
|
AppTheme {
|
||||||
|
ShoppingListCard(
|
||||||
|
listName = "Weekend shopping",
|
||||||
|
onClick = {},
|
||||||
|
onDelete = {},
|
||||||
|
onEdit = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
package gq.kirmanak.mealient.shopping_lists.ui.list
|
package gq.kirmanak.mealient.shopping_lists.ui.list
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
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.CreateShoppingListRequest
|
||||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListsSummaryResponse
|
import gq.kirmanak.mealient.datasource.models.GetShoppingListsSummaryResponse
|
||||||
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
@@ -15,12 +13,19 @@ import gq.kirmanak.mealient.shopping_lists.repo.ShoppingListsRepo
|
|||||||
import gq.kirmanak.mealient.ui.util.LoadingHelper
|
import gq.kirmanak.mealient.ui.util.LoadingHelper
|
||||||
import gq.kirmanak.mealient.ui.util.LoadingHelperFactory
|
import gq.kirmanak.mealient.ui.util.LoadingHelperFactory
|
||||||
import gq.kirmanak.mealient.ui.util.LoadingState
|
import gq.kirmanak.mealient.ui.util.LoadingState
|
||||||
|
import gq.kirmanak.mealient.ui.util.LoadingStateNoData
|
||||||
|
import gq.kirmanak.mealient.ui.util.map
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ShoppingListsViewModel @Inject constructor(
|
internal class ShoppingListsViewModel @Inject constructor(
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
private val shoppingListsRepo: ShoppingListsRepo,
|
private val shoppingListsRepo: ShoppingListsRepo,
|
||||||
private val authRepo: ShoppingListsAuthRepo,
|
private val authRepo: ShoppingListsAuthRepo,
|
||||||
@@ -32,15 +37,13 @@ class ShoppingListsViewModel @Inject constructor(
|
|||||||
runCatchingExceptCancel { shoppingListsRepo.getShoppingLists() }
|
runCatchingExceptCancel { shoppingListsRepo.getShoppingLists() }
|
||||||
}
|
}
|
||||||
|
|
||||||
val loadingState: StateFlow<LoadingState<List<GetShoppingListsSummaryResponse>>> =
|
private var _shoppingListsState = MutableStateFlow(ShoppingListsState())
|
||||||
loadingHelper.loadingState
|
val shoppingListsState: StateFlow<ShoppingListsState> get() = _shoppingListsState.asStateFlow()
|
||||||
|
|
||||||
private var _errorToShowInSnackbar by mutableStateOf<Throwable?>(null)
|
|
||||||
val errorToShowInSnackBar: Throwable? get() = _errorToShowInSnackbar
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
refresh()
|
refresh()
|
||||||
listenToAuthState()
|
listenToAuthState()
|
||||||
|
observeScreenState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenToAuthState() {
|
private fun listenToAuthState() {
|
||||||
@@ -53,15 +56,238 @@ class ShoppingListsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refresh() {
|
private fun observeScreenState() {
|
||||||
logger.v { "refresh() called" }
|
logger.v { "observeScreenState() called" }
|
||||||
viewModelScope.launch {
|
|
||||||
_errorToShowInSnackbar = loadingHelper.refresh().exceptionOrNull()
|
loadingHelper.loadingState.onEach { loadingState ->
|
||||||
|
logger.d { "screenStateUpdate: loadingState: $loadingState" }
|
||||||
|
val existingLists: LoadingState<List<DisplayList>> = loadingState.map { lists ->
|
||||||
|
lists.map { DisplayList(it) }
|
||||||
|
}
|
||||||
|
_shoppingListsState.update { it.copy(loadingState = existingLists) }
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEvent(event: ShoppingListsEvent) {
|
||||||
|
logger.v { "onEvent($event) called" }
|
||||||
|
when (event) {
|
||||||
|
is ShoppingListsEvent.AddShoppingList -> {
|
||||||
|
onAddShoppingListClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsEvent.NewListInput -> {
|
||||||
|
onNewListInput(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsEvent.SnackbarShown -> {
|
||||||
|
onSnackbarShown()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsEvent.RefreshRequested -> {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsEvent.NewListSaved -> {
|
||||||
|
onNewListSaved(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsEvent.DialogDismissed -> {
|
||||||
|
onDialogDismissed()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsEvent.EditList -> {
|
||||||
|
onEditList(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsEvent.RemoveList -> {
|
||||||
|
onRemoveList(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsEvent.RemoveListConfirmed -> {
|
||||||
|
onRemoveListConfirmed(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsEvent.EditListConfirmed -> {
|
||||||
|
onEditListConfirmed(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ShoppingListsEvent.EditListInput -> {
|
||||||
|
onEditListInput(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSnackbarShown() {
|
private fun onEditListConfirmed(event: ShoppingListsEvent.EditListConfirmed) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatchingExceptCancel {
|
||||||
|
shoppingListsRepo.updateShoppingListName(
|
||||||
|
id = event.displayList.id,
|
||||||
|
name = event.listName
|
||||||
|
)
|
||||||
|
}.onFailure { exception ->
|
||||||
|
logger.e(exception) { "Error while updating shopping list" }
|
||||||
|
_shoppingListsState.update { it.copy(errorToShow = exception) }
|
||||||
|
}.onSuccess {
|
||||||
|
refresh()
|
||||||
|
onDialogDismissed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRemoveListConfirmed(event: ShoppingListsEvent.RemoveListConfirmed) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatchingExceptCancel {
|
||||||
|
shoppingListsRepo.deleteShoppingList(event.displayList.id)
|
||||||
|
}.onFailure { exception ->
|
||||||
|
logger.e(exception) { "Error while deleting shopping list" }
|
||||||
|
_shoppingListsState.update { it.copy(errorToShow = exception) }
|
||||||
|
}.onSuccess {
|
||||||
|
refresh()
|
||||||
|
onDialogDismissed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEditListInput(event: ShoppingListsEvent.EditListInput) {
|
||||||
|
_shoppingListsState.update {
|
||||||
|
val old = it.dialog as? ShoppingListsDialog.EditListItem ?: return@update it
|
||||||
|
val onConfirm = old.onConfirm.copy(listName = event.newValue)
|
||||||
|
it.copy(dialog = old.copy(listName = event.newValue, onConfirm = onConfirm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRemoveList(event: ShoppingListsEvent.RemoveList) {
|
||||||
|
val onConfirm = ShoppingListsEvent.RemoveListConfirmed(
|
||||||
|
displayList = event.list
|
||||||
|
)
|
||||||
|
_shoppingListsState.update {
|
||||||
|
it.copy(dialog = ShoppingListsDialog.RemoveListItem(event.list.name, onConfirm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEditList(event: ShoppingListsEvent.EditList) {
|
||||||
|
val name = event.list.name
|
||||||
|
val onConfirm = ShoppingListsEvent.EditListConfirmed(
|
||||||
|
listName = name,
|
||||||
|
displayList = event.list
|
||||||
|
)
|
||||||
|
_shoppingListsState.update {
|
||||||
|
it.copy(dialog = ShoppingListsDialog.EditListItem(name, name, onConfirm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDialogDismissed() {
|
||||||
|
_shoppingListsState.update {
|
||||||
|
it.copy(dialog = ShoppingListsDialog.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onNewListSaved(event: ShoppingListsEvent.NewListSaved) {
|
||||||
|
logger.v { "onNewListSaved($event) called" }
|
||||||
|
val request = CreateShoppingListRequest(event.name)
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatchingExceptCancel {
|
||||||
|
shoppingListsRepo.addShoppingList(request)
|
||||||
|
}.onFailure { exception ->
|
||||||
|
logger.e(exception) { "Error while creating shopping list" }
|
||||||
|
_shoppingListsState.update { it.copy(errorToShow = exception) }
|
||||||
|
}.onSuccess {
|
||||||
|
logger.d { "Shopping list \"${request.name}\" created" }
|
||||||
|
refresh()
|
||||||
|
onDialogDismissed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
logger.v { "refresh() called" }
|
||||||
|
viewModelScope.launch {
|
||||||
|
val errorToShow = loadingHelper.refresh().exceptionOrNull()
|
||||||
|
_shoppingListsState.update {
|
||||||
|
it.copy(errorToShow = errorToShow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSnackbarShown() {
|
||||||
logger.v { "onSnackbarShown() called" }
|
logger.v { "onSnackbarShown() called" }
|
||||||
_errorToShowInSnackbar = null
|
_shoppingListsState.update {
|
||||||
|
it.copy(errorToShow = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onNewListInput(event: ShoppingListsEvent.NewListInput) {
|
||||||
|
logger.v { "onNewListNameChanged($event) called" }
|
||||||
|
val filteredName = event.newName.replace(System.lineSeparator(), "")
|
||||||
|
_shoppingListsState.update {
|
||||||
|
it.copy(dialog = ShoppingListsDialog.NewListItem(filteredName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAddShoppingListClicked() {
|
||||||
|
logger.v { "onAddShoppingListClicked() called" }
|
||||||
|
_shoppingListsState.update {
|
||||||
|
it.copy(dialog = ShoppingListsDialog.NewListItem(""))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal data class DisplayList(
|
||||||
|
private val list: GetShoppingListsSummaryResponse,
|
||||||
|
val name: String = list.name.orEmpty(),
|
||||||
|
val id: String = list.id
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class ShoppingListsState(
|
||||||
|
val errorToShow: Throwable? = null,
|
||||||
|
val loadingState: LoadingState<List<DisplayList>> = LoadingStateNoData.InitialLoad,
|
||||||
|
val dialog: ShoppingListsDialog = ShoppingListsDialog.None
|
||||||
|
)
|
||||||
|
|
||||||
|
internal sealed interface ShoppingListsEvent {
|
||||||
|
|
||||||
|
data object AddShoppingList : ShoppingListsEvent
|
||||||
|
|
||||||
|
data class NewListInput(val newName: String) : ShoppingListsEvent
|
||||||
|
|
||||||
|
data object SnackbarShown : ShoppingListsEvent
|
||||||
|
|
||||||
|
data object RefreshRequested : ShoppingListsEvent
|
||||||
|
|
||||||
|
data class NewListSaved(val name: String) : ShoppingListsEvent
|
||||||
|
|
||||||
|
data object DialogDismissed : ShoppingListsEvent
|
||||||
|
|
||||||
|
data class EditList(val list: DisplayList) : ShoppingListsEvent
|
||||||
|
|
||||||
|
data class RemoveList(val list: DisplayList) : ShoppingListsEvent
|
||||||
|
|
||||||
|
data class EditListInput(val newValue: String) : ShoppingListsEvent
|
||||||
|
|
||||||
|
data class RemoveListConfirmed(
|
||||||
|
val displayList: DisplayList,
|
||||||
|
) : ShoppingListsEvent
|
||||||
|
|
||||||
|
data class EditListConfirmed(
|
||||||
|
val listName: String,
|
||||||
|
val displayList: DisplayList,
|
||||||
|
) : ShoppingListsEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed interface ShoppingListsDialog {
|
||||||
|
|
||||||
|
data class NewListItem(val listName: String) : ShoppingListsDialog
|
||||||
|
|
||||||
|
data class EditListItem(
|
||||||
|
val listName: String,
|
||||||
|
val oldListName: String,
|
||||||
|
val onConfirm: ShoppingListsEvent.EditListConfirmed
|
||||||
|
) : ShoppingListsDialog
|
||||||
|
|
||||||
|
data class RemoveListItem(
|
||||||
|
val listName: String,
|
||||||
|
val onConfirm: ShoppingListsEvent.RemoveListConfirmed
|
||||||
|
) : ShoppingListsDialog
|
||||||
|
|
||||||
|
data object None : ShoppingListsDialog
|
||||||
|
}
|
||||||
@@ -22,4 +22,18 @@
|
|||||||
<string name="shopping_lists_screen_no_connection">No server connection</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_unknown_error">Unknown error</string>
|
||||||
<string name="shopping_lists_screen_empty_button_refresh">Try again</string>
|
<string name="shopping_lists_screen_empty_button_refresh">Try again</string>
|
||||||
|
<string name="shopping_lists_screen_add_icon_content_description">Add list</string>
|
||||||
|
<string name="shopping_lists_screen_list_name_placeholder">List name</string>
|
||||||
|
<string name="shopping_lists_screen_list_name_done_content_description">Done</string>
|
||||||
|
<string name="shopping_lists_dialog_delete_icon_content_description">Delete %1$s</string>
|
||||||
|
<string name="shopping_lists_dialog_edit_icon_content_description">Rename %1$s</string>
|
||||||
|
|
||||||
|
<string name="shopping_lists_dialog_add_new_title">Add new shopping list</string>
|
||||||
|
<string name="shopping_lists_dialog_edit_name_title">Edit %1$s</string>
|
||||||
|
<string name="shopping_lists_dialog_confirm">Confirm</string>
|
||||||
|
<string name="shopping_lists_dialog_cancel">Cancel</string>
|
||||||
|
<string name="shopping_lists_dialog_name_clear_input">Clear input</string>
|
||||||
|
<string name="shopping_lists_dialog_delete_confirm_title">Removing %1$s</string>
|
||||||
|
<string name="shopping_lists_dialog_delete_confirm_text">Confirm that you want to remove the shopping list including all items.</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshState
|
import androidx.compose.material.pullrefresh.PullRefreshState
|
||||||
@@ -22,6 +24,7 @@ fun LazyColumnPullRefresh(
|
|||||||
verticalArrangement: Arrangement.Vertical,
|
verticalArrangement: Arrangement.Vertical,
|
||||||
lazyColumnContent: LazyListScope.() -> Unit,
|
lazyColumnContent: LazyListScope.() -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
lazyListState: LazyListState = rememberLazyListState(),
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier.pullRefresh(refreshState),
|
modifier = modifier.pullRefresh(refreshState),
|
||||||
@@ -29,6 +32,7 @@ fun LazyColumnPullRefresh(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
verticalArrangement = verticalArrangement,
|
verticalArrangement = verticalArrangement,
|
||||||
|
state = lazyListState,
|
||||||
content = lazyColumnContent
|
content = lazyColumnContent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
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.FabPosition
|
||||||
@@ -35,6 +37,7 @@ fun <T> LazyColumnWithLoadingState(
|
|||||||
onRefresh: () -> Unit = {},
|
onRefresh: () -> Unit = {},
|
||||||
floatingActionButton: @Composable () -> Unit = {},
|
floatingActionButton: @Composable () -> Unit = {},
|
||||||
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
||||||
|
lazyListState: LazyListState = rememberLazyListState(),
|
||||||
lazyColumnContent: LazyListScope.(List<T>) -> Unit = {},
|
lazyColumnContent: LazyListScope.(List<T>) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val refreshState = rememberPullRefreshState(
|
val refreshState = rememberPullRefreshState(
|
||||||
@@ -76,6 +79,7 @@ fun <T> LazyColumnWithLoadingState(
|
|||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
verticalArrangement = verticalArrangement,
|
verticalArrangement = verticalArrangement,
|
||||||
lazyColumnContent = { lazyColumnContent(list) },
|
lazyColumnContent = { lazyColumnContent(list) },
|
||||||
|
lazyListState = lazyListState,
|
||||||
modifier = innerModifier,
|
modifier = innerModifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user