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.material3.Card
|
||||
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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -73,7 +74,9 @@ private fun IngredientListItem(
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
|
||||
Divider()
|
||||
HorizontalDivider(
|
||||
color = LocalContentColor.current
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
|
||||
@@ -5,7 +5,8 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -86,7 +87,9 @@ private fun InstructionListItem(
|
||||
)
|
||||
|
||||
if (ingredients.isNotEmpty()) {
|
||||
Divider()
|
||||
HorizontalDivider(
|
||||
color = LocalContentColor.current
|
||||
)
|
||||
ingredients.forEach { ingredient ->
|
||||
Text(
|
||||
text = ingredient.display,
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<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_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_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>
|
||||
|
||||
@@ -4,6 +4,7 @@ import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
|
||||
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
|
||||
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
|
||||
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.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
|
||||
@@ -75,4 +76,10 @@ interface MealieDataSource {
|
||||
suspend fun getUnits(): GetUnitsResponse
|
||||
|
||||
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.CreateRecipeRequest
|
||||
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.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipesResponse
|
||||
@@ -61,4 +62,12 @@ internal interface MealieService {
|
||||
suspend fun getUnits(perPage: Int): GetUnitsResponse
|
||||
|
||||
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.CreateRecipeRequest
|
||||
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.GetFoodsResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||
@@ -37,7 +38,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
) : MealieDataSource {
|
||||
|
||||
override suspend fun createRecipe(
|
||||
recipe: CreateRecipeRequest
|
||||
recipe: CreateRecipeRequest,
|
||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.createRecipe(recipe) },
|
||||
logMethod = { "createRecipe" },
|
||||
@@ -46,7 +47,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
|
||||
override suspend fun updateRecipe(
|
||||
slug: String,
|
||||
recipe: UpdateRecipeRequest
|
||||
recipe: UpdateRecipeRequest,
|
||||
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.updateRecipe(recipe, slug) },
|
||||
logMethod = { "updateRecipe" },
|
||||
@@ -80,7 +81,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
|
||||
override suspend fun requestRecipes(
|
||||
page: Int,
|
||||
perPage: Int
|
||||
perPage: Int,
|
||||
): List<GetRecipeSummaryResponse> = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.getRecipeSummary(page, perPage) },
|
||||
logMethod = { "requestRecipes" },
|
||||
@@ -88,7 +89,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
).items
|
||||
|
||||
override suspend fun requestRecipeInfo(
|
||||
slug: String
|
||||
slug: String,
|
||||
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.getRecipe(slug) },
|
||||
logMethod = { "requestRecipeInfo" },
|
||||
@@ -96,7 +97,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
)
|
||||
|
||||
override suspend fun parseRecipeFromURL(
|
||||
request: ParseRecipeURLRequest
|
||||
request: ParseRecipeURLRequest,
|
||||
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.createRecipeFromURL(request) },
|
||||
logMethod = { "parseRecipeFromURL" },
|
||||
@@ -104,7 +105,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
)
|
||||
|
||||
override suspend fun createApiToken(
|
||||
request: CreateApiTokenRequest
|
||||
request: CreateApiTokenRequest,
|
||||
): CreateApiTokenResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.createApiToken(request) },
|
||||
logMethod = { "createApiToken" },
|
||||
@@ -120,7 +121,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
|
||||
override suspend fun removeFavoriteRecipe(
|
||||
userId: String,
|
||||
recipeSlug: String
|
||||
recipeSlug: String,
|
||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.removeFavoriteRecipe(userId, recipeSlug) },
|
||||
logMethod = { "removeFavoriteRecipe" },
|
||||
@@ -129,7 +130,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
|
||||
override suspend fun addFavoriteRecipe(
|
||||
userId: String,
|
||||
recipeSlug: String
|
||||
recipeSlug: String,
|
||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.addFavoriteRecipe(userId, recipeSlug) },
|
||||
logMethod = { "addFavoriteRecipe" },
|
||||
@@ -137,7 +138,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
)
|
||||
|
||||
override suspend fun deleteRecipe(
|
||||
slug: String
|
||||
slug: String,
|
||||
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.deleteRecipe(slug) },
|
||||
logMethod = { "deleteRecipe" },
|
||||
@@ -154,7 +155,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
)
|
||||
|
||||
override suspend fun getShoppingList(
|
||||
id: String
|
||||
id: String,
|
||||
): GetShoppingListResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.getShoppingList(id) },
|
||||
logMethod = { "getShoppingList" },
|
||||
@@ -187,7 +188,7 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
)
|
||||
|
||||
override suspend fun updateShoppingListItem(
|
||||
item: GetShoppingListItemResponse
|
||||
item: GetShoppingListItemResponse,
|
||||
) {
|
||||
// Has to be done in two steps because we can't specify only the changed fields
|
||||
val remoteItem = getShoppingListItem(item.id)
|
||||
@@ -219,10 +220,55 @@ internal class MealieDataSourceImpl @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun addShoppingListItem(
|
||||
request: CreateShoppingListItemRequest
|
||||
request: CreateShoppingListItemRequest,
|
||||
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||
block = { service.createShoppingListItem(request) },
|
||||
logMethod = { "addShoppingListItem" },
|
||||
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.CreateRecipeRequest
|
||||
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.GetRecipeResponse
|
||||
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(
|
||||
path: String,
|
||||
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
|
||||
|
||||
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.GetShoppingListItemResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||
@@ -22,4 +23,10 @@ interface ShoppingListsDataSource {
|
||||
suspend fun getUnits(): List<GetUnitResponse>
|
||||
|
||||
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.models.CreateShoppingListItemRequest
|
||||
import gq.kirmanak.mealient.datasource.models.CreateShoppingListRequest
|
||||
import gq.kirmanak.mealient.datasource.models.GetFoodResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||
@@ -27,7 +28,7 @@ class ShoppingListsDataSourceImpl @Inject constructor(
|
||||
) = dataSource.deleteShoppingListItem(id)
|
||||
|
||||
override suspend fun updateShoppingListItem(
|
||||
item: GetShoppingListItemResponse
|
||||
item: GetShoppingListItemResponse,
|
||||
) = dataSource.updateShoppingListItem(item)
|
||||
|
||||
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 addShoppingListItem(
|
||||
item: CreateShoppingListItemRequest
|
||||
item: CreateShoppingListItemRequest,
|
||||
) = 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
|
||||
|
||||
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.GetShoppingListItemResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||
@@ -22,4 +23,10 @@ interface ShoppingListsRepo {
|
||||
suspend fun getUnits(): List<GetUnitResponse>
|
||||
|
||||
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
|
||||
|
||||
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.GetShoppingListItemResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
|
||||
@@ -49,4 +50,20 @@ class ShoppingListsRepoImpl @Inject constructor(
|
||||
logger.v { "addShoppingListItem() called with: item = $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
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.lazy.rememberLazyListState
|
||||
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
|
||||
@@ -31,15 +26,12 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.GetUnitResponse
|
||||
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.ui.AppTheme
|
||||
import gq.kirmanak.mealient.ui.Dimens
|
||||
@@ -101,7 +94,6 @@ internal fun ShoppingListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ShoppingListScreen(
|
||||
loadingState: LoadingState<ShoppingListScreenState>,
|
||||
@@ -123,6 +115,12 @@ private fun ShoppingListScreen(
|
||||
loadingState.data?.name.orEmpty()
|
||||
)
|
||||
|
||||
var lastAddedItemIndex by remember { mutableIntStateOf(-1) }
|
||||
val lazyListState = rememberLazyListState()
|
||||
LaunchedEffect(lastAddedItemIndex) {
|
||||
if (lastAddedItemIndex >= 0) lazyListState.animateScrollToItem(lastAddedItemIndex)
|
||||
}
|
||||
|
||||
LazyColumnWithLoadingState(
|
||||
modifier = modifier,
|
||||
loadingState = loadingState.map { it.items },
|
||||
@@ -145,9 +143,12 @@ private fun ShoppingListScreen(
|
||||
contentDescription = stringResource(id = R.string.shopping_list_screen_add_icon_content_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
lazyListState = lazyListState
|
||||
) { items ->
|
||||
val firstCheckedItemIndex = items.indexOfFirst { it.checked }
|
||||
lastAddedItemIndex =
|
||||
items.indexOfLast { it is ShoppingListItemState.NewItem }
|
||||
|
||||
itemsIndexed(items, { _, item -> item.id }) { index, itemState ->
|
||||
if (itemState is ShoppingListItemState.ExistingItem) {
|
||||
@@ -507,7 +508,6 @@ fun ShoppingListItemEditorNonFoodPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ShoppingListItem(
|
||||
itemState: ShoppingListItemState.ExistingItem,
|
||||
@@ -516,57 +516,13 @@ fun ShoppingListItem(
|
||||
onCheckedChange: (Boolean) -> Unit = {},
|
||||
onDismissed: () -> Unit = {},
|
||||
onEditStart: () -> Unit = {},
|
||||
dismissState: SwipeToDismissBoxState = rememberSwipeToDismissBoxState(
|
||||
confirmValueChange = {
|
||||
when (it) {
|
||||
SwipeToDismissBoxValue.EndToStart -> onDismissed()
|
||||
SwipeToDismissBoxValue.StartToEnd -> onEditStart()
|
||||
SwipeToDismissBoxValue.Settled -> Unit
|
||||
}
|
||||
true
|
||||
}
|
||||
),
|
||||
) {
|
||||
val shoppingListItem = itemState.item
|
||||
SwipeToDismissBox(
|
||||
state = dismissState,
|
||||
backgroundContent = {
|
||||
if (dismissState.targetValue == SwipeToDismissBoxValue.EndToStart) {
|
||||
val color by animateColorAsState(MaterialTheme.colorScheme.error)
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
EditableItemBox(
|
||||
modifier = modifier,
|
||||
onDelete = onDismissed,
|
||||
onEdit = onEditStart,
|
||||
deleteContentDescription = stringResource(R.string.shopping_list_screen_delete_icon_content_description),
|
||||
editContentDescription = stringResource(R.string.shopping_list_screen_edit_icon_content_description),
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -585,6 +541,7 @@ fun ShoppingListItem(
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
|
||||
val shoppingListItem = itemState.item
|
||||
val isFood = shoppingListItem.isFood
|
||||
val quantity = shoppingListItem.quantity
|
||||
.takeUnless { it == 0.0 }
|
||||
@@ -601,11 +558,9 @@ fun ShoppingListItem(
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ColorSchemePreview
|
||||
fun PreviewShoppingListItemChecked() {
|
||||
@@ -617,7 +572,6 @@ fun PreviewShoppingListItemChecked() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ColorSchemePreview
|
||||
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 {
|
||||
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ShoppingCart
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -21,8 +24,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
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_lists.ui.composables.EditableItemBox
|
||||
import gq.kirmanak.mealient.shopping_lists.ui.composables.getErrorMessage
|
||||
import gq.kirmanak.mealient.shopping_lists.ui.destinations.ShoppingListScreenDestination
|
||||
import gq.kirmanak.mealient.ui.AppTheme
|
||||
@@ -35,33 +38,57 @@ import gq.kirmanak.mealient.ui.util.error
|
||||
|
||||
@Destination
|
||||
@Composable
|
||||
fun ShoppingListsScreen(
|
||||
internal fun ShoppingListsScreen(
|
||||
navController: NavController,
|
||||
baseScreenState: BaseScreenState,
|
||||
shoppingListsViewModel: ShoppingListsViewModel = hiltViewModel(),
|
||||
) {
|
||||
val loadingState by shoppingListsViewModel.loadingState.collectAsState()
|
||||
val errorToShowInSnackbar = shoppingListsViewModel.errorToShowInSnackBar
|
||||
val screenState by shoppingListsViewModel.shoppingListsState.collectAsState()
|
||||
|
||||
ShoppingListsScreenDialog(
|
||||
dialog = screenState.dialog,
|
||||
onEvent = shoppingListsViewModel::onEvent
|
||||
)
|
||||
|
||||
BaseScreenWithNavigation(
|
||||
baseScreenState = baseScreenState,
|
||||
) { modifier ->
|
||||
LazyColumnWithLoadingState(
|
||||
modifier = modifier,
|
||||
loadingState = loadingState,
|
||||
emptyListError = loadingState.error?.let { getErrorMessage(it) }
|
||||
loadingState = screenState.loadingState,
|
||||
emptyListError = screenState.loadingState.error?.let { getErrorMessage(it) }
|
||||
?: stringResource(R.string.shopping_lists_screen_empty),
|
||||
retryButtonText = stringResource(id = R.string.shopping_lists_screen_empty_button_refresh),
|
||||
snackbarText = errorToShowInSnackbar?.let { getErrorMessage(error = it) },
|
||||
onSnackbarShown = shoppingListsViewModel::onSnackbarShown,
|
||||
onRefresh = shoppingListsViewModel::refresh
|
||||
snackbarText = screenState.errorToShow?.let { getErrorMessage(error = it) },
|
||||
onSnackbarShown = { shoppingListsViewModel.onEvent(ShoppingListsEvent.SnackbarShown) },
|
||||
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) { shoppingList ->
|
||||
items(
|
||||
items = items,
|
||||
key = { it.id },
|
||||
contentType = { "Existing list" }
|
||||
) { displayList ->
|
||||
ShoppingListCard(
|
||||
shoppingList = shoppingList,
|
||||
onItemClick = { clickedEntity ->
|
||||
val shoppingListId = clickedEntity.id
|
||||
listName = displayList.name,
|
||||
onClick = {
|
||||
val shoppingListId = displayList.id
|
||||
navController.navigate(ShoppingListScreenDestination(shoppingListId))
|
||||
},
|
||||
onDelete = {
|
||||
shoppingListsViewModel.onEvent(ShoppingListsEvent.RemoveList(displayList))
|
||||
},
|
||||
onEdit = {
|
||||
shoppingListsViewModel.onEvent(ShoppingListsEvent.EditList(displayList))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -70,41 +97,122 @@ fun ShoppingListsScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ColorSchemePreview
|
||||
private fun PreviewShoppingListCard() {
|
||||
AppTheme {
|
||||
ShoppingListCard(
|
||||
shoppingList = GetShoppingListsSummaryResponse("1", "Weekend shopping"),
|
||||
)
|
||||
private fun ShoppingListsScreenDialog(
|
||||
dialog: ShoppingListsDialog,
|
||||
onEvent: (ShoppingListsEvent) -> Unit,
|
||||
) {
|
||||
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
|
||||
private fun ShoppingListCard(
|
||||
shoppingList: GetShoppingListsSummaryResponse?,
|
||||
listName: String,
|
||||
onClick: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onEdit: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onItemClick: (GetShoppingListsSummaryResponse) -> Unit = {},
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.padding(horizontal = Dimens.Medium, vertical = Dimens.Small)
|
||||
.fillMaxWidth()
|
||||
.clickable { shoppingList?.let { onItemClick(it) } },
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(Dimens.Medium),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ShoppingCart,
|
||||
contentDescription = stringResource(id = R.string.shopping_lists_screen_cart_icon),
|
||||
modifier = Modifier.height(Dimens.Large),
|
||||
)
|
||||
Text(
|
||||
text = shoppingList?.name.orEmpty(),
|
||||
modifier = Modifier.padding(start = Dimens.Medium),
|
||||
)
|
||||
}
|
||||
EditableItemBox(
|
||||
modifier = modifier,
|
||||
onDelete = onDelete,
|
||||
onEdit = onEdit,
|
||||
deleteContentDescription = stringResource(
|
||||
id = R.string.shopping_list_screen_delete_icon_content_description,
|
||||
listName
|
||||
),
|
||||
editContentDescription = stringResource(
|
||||
id = R.string.shopping_list_screen_edit_icon_content_description,
|
||||
listName
|
||||
),
|
||||
content = {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = 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
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
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.CreateShoppingListRequest
|
||||
import gq.kirmanak.mealient.datasource.models.GetShoppingListsSummaryResponse
|
||||
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
|
||||
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.LoadingHelperFactory
|
||||
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.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ShoppingListsViewModel @Inject constructor(
|
||||
internal class ShoppingListsViewModel @Inject constructor(
|
||||
private val logger: Logger,
|
||||
private val shoppingListsRepo: ShoppingListsRepo,
|
||||
private val authRepo: ShoppingListsAuthRepo,
|
||||
@@ -32,15 +37,13 @@ class ShoppingListsViewModel @Inject constructor(
|
||||
runCatchingExceptCancel { shoppingListsRepo.getShoppingLists() }
|
||||
}
|
||||
|
||||
val loadingState: StateFlow<LoadingState<List<GetShoppingListsSummaryResponse>>> =
|
||||
loadingHelper.loadingState
|
||||
|
||||
private var _errorToShowInSnackbar by mutableStateOf<Throwable?>(null)
|
||||
val errorToShowInSnackBar: Throwable? get() = _errorToShowInSnackbar
|
||||
private var _shoppingListsState = MutableStateFlow(ShoppingListsState())
|
||||
val shoppingListsState: StateFlow<ShoppingListsState> get() = _shoppingListsState.asStateFlow()
|
||||
|
||||
init {
|
||||
refresh()
|
||||
listenToAuthState()
|
||||
observeScreenState()
|
||||
}
|
||||
|
||||
private fun listenToAuthState() {
|
||||
@@ -53,15 +56,238 @@ class ShoppingListsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
logger.v { "refresh() called" }
|
||||
viewModelScope.launch {
|
||||
_errorToShowInSnackbar = loadingHelper.refresh().exceptionOrNull()
|
||||
private fun observeScreenState() {
|
||||
logger.v { "observeScreenState() called" }
|
||||
|
||||
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" }
|
||||
_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_unknown_error">Unknown error</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>
|
||||
@@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.PullRefreshState
|
||||
@@ -22,6 +24,7 @@ fun LazyColumnPullRefresh(
|
||||
verticalArrangement: Arrangement.Vertical,
|
||||
lazyColumnContent: LazyListScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
lazyListState: LazyListState = rememberLazyListState(),
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.pullRefresh(refreshState),
|
||||
@@ -29,6 +32,7 @@ fun LazyColumnPullRefresh(
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
verticalArrangement = verticalArrangement,
|
||||
state = lazyListState,
|
||||
content = lazyColumnContent
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.FabPosition
|
||||
@@ -35,6 +37,7 @@ fun <T> LazyColumnWithLoadingState(
|
||||
onRefresh: () -> Unit = {},
|
||||
floatingActionButton: @Composable () -> Unit = {},
|
||||
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
||||
lazyListState: LazyListState = rememberLazyListState(),
|
||||
lazyColumnContent: LazyListScope.(List<T>) -> Unit = {},
|
||||
) {
|
||||
val refreshState = rememberPullRefreshState(
|
||||
@@ -76,6 +79,7 @@ fun <T> LazyColumnWithLoadingState(
|
||||
contentPadding = contentPadding,
|
||||
verticalArrangement = verticalArrangement,
|
||||
lazyColumnContent = { lazyColumnContent(list) },
|
||||
lazyListState = lazyListState,
|
||||
modifier = innerModifier,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user