diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/IngredientsSection.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/IngredientsSection.kt
index fc7aeb4..bdeb517 100644
--- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/IngredientsSection.kt
+++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/IngredientsSection.kt
@@ -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(
diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/InstructionsSection.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/InstructionsSection.kt
index 1fe146d..d3d1b2b 100644
--- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/InstructionsSection.kt
+++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/InstructionsSection.kt
@@ -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,
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b3d921f..7d17d6f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -45,7 +45,7 @@
Clear
Example: demo.mealie.io
Example: changeme@example.com
- Example: demo
+ Example: MyPassword
Last page loaded
Load error: %1$s.
Load failed.
diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt
index 807262e..6ac793e 100644
--- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt
+++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt
@@ -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)
}
\ No newline at end of file
diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt
index d4749aa..0717ec8 100644
--- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt
+++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt
@@ -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
}
\ No newline at end of file
diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieDataSourceImpl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieDataSourceImpl.kt
index 2bd6864..8f6149f 100644
--- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieDataSourceImpl.kt
+++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieDataSourceImpl.kt
@@ -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 = 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)
+ }
}
diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieServiceKtor.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieServiceKtor.kt
index 3af8013..32269ce 100644
--- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieServiceKtor.kt
+++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/impl/MealieServiceKtor.kt
@@ -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 = {},
diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/CreateShoppingListRequest.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/CreateShoppingListRequest.kt
new file mode 100644
index 0000000..f114b98
--- /dev/null
+++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/CreateShoppingListRequest.kt
@@ -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,
+)
diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSource.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSource.kt
index a11eb3e..87540c6 100644
--- a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSource.kt
+++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSource.kt
@@ -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
suspend fun addShoppingListItem(item: CreateShoppingListItemRequest)
+
+ suspend fun addShoppingList(request: CreateShoppingListRequest)
+
+ suspend fun deleteShoppingList(id: String)
+
+ suspend fun updateShoppingListName(id: String, name: String)
}
\ No newline at end of file
diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSourceImpl.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSourceImpl.kt
index 693d7d9..5c914ef 100644
--- a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSourceImpl.kt
+++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/network/ShoppingListsDataSourceImpl.kt
@@ -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 = dataSource.getFoods().items
@@ -35,7 +36,18 @@ class ShoppingListsDataSourceImpl @Inject constructor(
override suspend fun getUnits(): List = 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)
}
diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepo.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepo.kt
index c82f03c..ae4f260 100644
--- a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepo.kt
+++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepo.kt
@@ -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
suspend fun addShoppingListItem(item: CreateShoppingListItemRequest)
+
+ suspend fun addShoppingList(request: CreateShoppingListRequest)
+
+ suspend fun deleteShoppingList(id: String)
+
+ suspend fun updateShoppingListName(id: String, name: String)
}
\ No newline at end of file
diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepoImpl.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepoImpl.kt
index dc4f23a..9565bf2 100644
--- a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepoImpl.kt
+++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/repo/ShoppingListsRepoImpl.kt
@@ -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)
+ }
}
\ No newline at end of file
diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/EditableItemBox.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/EditableItemBox.kt
new file mode 100644
index 0000000..9c7072a
--- /dev/null
+++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/EditableItemBox.kt
@@ -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
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/MealientTextField.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/MealientTextField.kt
new file mode 100644
index 0000000..84c9802
--- /dev/null
+++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/composables/MealientTextField.kt
@@ -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
+ )
+}
\ No newline at end of file
diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/details/ShoppingListScreen.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/details/ShoppingListScreen.kt
index 55e6edb..b1230a5 100644
--- a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/details/ShoppingListScreen.kt
+++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/details/ShoppingListScreen.kt
@@ -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,
@@ -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(
diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/DeleteListConfirmDialog.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/DeleteListConfirmDialog.kt
new file mode 100644
index 0000000..1ede374
--- /dev/null
+++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/DeleteListConfirmDialog.kt
@@ -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"
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/ShoppingListNameDialog.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/ShoppingListNameDialog.kt
new file mode 100644
index 0000000..81a5cb4
--- /dev/null
+++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/ShoppingListNameDialog.kt
@@ -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 = {},
+ )
+ }
+}
diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/ShoppingListsScreen.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/ShoppingListsScreen.kt
index 9973cd5..4b3dde8 100644
--- a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/ShoppingListsScreen.kt
+++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/ShoppingListsScreen.kt
@@ -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 = {}
+ )
}
}
diff --git a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/ShoppingListsViewModel.kt b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/ShoppingListsViewModel.kt
index 767933e..0f6cdf8 100644
--- a/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/ShoppingListsViewModel.kt
+++ b/features/shopping_lists/src/main/kotlin/gq/kirmanak/mealient/shopping_lists/ui/list/ShoppingListsViewModel.kt
@@ -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>> =
- loadingHelper.loadingState
-
- private var _errorToShowInSnackbar by mutableStateOf(null)
- val errorToShowInSnackBar: Throwable? get() = _errorToShowInSnackbar
+ private var _shoppingListsState = MutableStateFlow(ShoppingListsState())
+ val shoppingListsState: StateFlow 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> = 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> = 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
+}
\ No newline at end of file
diff --git a/features/shopping_lists/src/main/res/values/strings.xml b/features/shopping_lists/src/main/res/values/strings.xml
index 21608ee..c816921 100644
--- a/features/shopping_lists/src/main/res/values/strings.xml
+++ b/features/shopping_lists/src/main/res/values/strings.xml
@@ -22,4 +22,18 @@
No server connection
Unknown error
Try again
+ Add list
+ List name
+ Done
+ Delete %1$s
+ Rename %1$s
+
+ Add new shopping list
+ Edit %1$s
+ Confirm
+ Cancel
+ Clear input
+ Removing %1$s
+ Confirm that you want to remove the shopping list including all items.
+
\ No newline at end of file
diff --git a/ui/src/main/kotlin/gq/kirmanak/mealient/ui/components/LazyColumnPullRefresh.kt b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/components/LazyColumnPullRefresh.kt
index 8118e93..eeedabc 100644
--- a/ui/src/main/kotlin/gq/kirmanak/mealient/ui/components/LazyColumnPullRefresh.kt
+++ b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/components/LazyColumnPullRefresh.kt
@@ -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
)
diff --git a/ui/src/main/kotlin/gq/kirmanak/mealient/ui/components/LazyColumnWithLoadingState.kt b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/components/LazyColumnWithLoadingState.kt
index 6f72117..4e67ceb 100644
--- a/ui/src/main/kotlin/gq/kirmanak/mealient/ui/components/LazyColumnWithLoadingState.kt
+++ b/ui/src/main/kotlin/gq/kirmanak/mealient/ui/components/LazyColumnWithLoadingState.kt
@@ -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 LazyColumnWithLoadingState(
onRefresh: () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
+ lazyListState: LazyListState = rememberLazyListState(),
lazyColumnContent: LazyListScope.(List) -> Unit = {},
) {
val refreshState = rememberPullRefreshState(
@@ -76,6 +79,7 @@ fun LazyColumnWithLoadingState(
contentPadding = contentPadding,
verticalArrangement = verticalArrangement,
lazyColumnContent = { lazyColumnContent(list) },
+ lazyListState = lazyListState,
modifier = innerModifier,
)