1.0.0 - Material you rework
This commit is contained in:
@@ -6,8 +6,10 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
@@ -17,6 +19,8 @@ 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
|
||||
@@ -60,7 +64,7 @@ import com.atridad.mealient.shopping_lists.ui.composables.getErrorMessage
|
||||
import com.atridad.mealient.shopping_lists.util.ItemLabelGroup
|
||||
import com.atridad.mealient.ui.AppTheme
|
||||
import com.atridad.mealient.ui.Dimens
|
||||
import com.atridad.mealient.ui.components.BaseScreen
|
||||
|
||||
import com.atridad.mealient.ui.components.LazyColumnWithLoadingState
|
||||
import com.atridad.mealient.ui.preview.ColorSchemePreview
|
||||
import com.atridad.mealient.ui.util.LoadingState
|
||||
@@ -68,6 +72,7 @@ import com.atridad.mealient.ui.util.data
|
||||
import com.atridad.mealient.ui.util.error
|
||||
import com.atridad.mealient.ui.util.map
|
||||
import java.text.DecimalFormat
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
data class ShoppingListNavArgs(
|
||||
val shoppingListId: String,
|
||||
@@ -82,25 +87,24 @@ internal fun ShoppingListScreen(
|
||||
) {
|
||||
val loadingState by shoppingListViewModel.loadingState.collectAsState()
|
||||
|
||||
BaseScreen { modifier ->
|
||||
ShoppingListScreen(
|
||||
modifier = modifier,
|
||||
loadingState = loadingState,
|
||||
errorToShowInSnackbar = shoppingListViewModel.errorToShowInSnackbar,
|
||||
onSnackbarShown = shoppingListViewModel::onSnackbarShown,
|
||||
onRefreshRequest = shoppingListViewModel::refreshShoppingList,
|
||||
onAddItemClicked = shoppingListViewModel::onAddItemClicked,
|
||||
onEditCancel = shoppingListViewModel::onEditCancel,
|
||||
onEditConfirm = shoppingListViewModel::onEditConfirm,
|
||||
onItemCheckedChange = shoppingListViewModel::onItemCheckedChange,
|
||||
onDeleteItem = shoppingListViewModel::deleteShoppingListItem,
|
||||
onEditStart = shoppingListViewModel::onEditStart,
|
||||
onAddCancel = shoppingListViewModel::onAddCancel,
|
||||
onAddConfirm = shoppingListViewModel::onAddConfirm,
|
||||
)
|
||||
}
|
||||
ShoppingListScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
loadingState = loadingState,
|
||||
errorToShowInSnackbar = shoppingListViewModel.errorToShowInSnackbar,
|
||||
onSnackbarShown = shoppingListViewModel::onSnackbarShown,
|
||||
onRefreshRequest = shoppingListViewModel::refreshShoppingList,
|
||||
onAddItemClicked = shoppingListViewModel::onAddItemClicked,
|
||||
onEditCancel = shoppingListViewModel::onEditCancel,
|
||||
onEditConfirm = shoppingListViewModel::onEditConfirm,
|
||||
onItemCheckedChange = shoppingListViewModel::onItemCheckedChange,
|
||||
onDeleteItem = shoppingListViewModel::deleteShoppingListItem,
|
||||
onEditStart = shoppingListViewModel::onEditStart,
|
||||
onAddCancel = shoppingListViewModel::onAddCancel,
|
||||
onAddConfirm = shoppingListViewModel::onAddConfirm,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ShoppingListScreen(
|
||||
loadingState: LoadingState<ShoppingListScreenState>,
|
||||
@@ -117,6 +121,27 @@ private fun ShoppingListScreen(
|
||||
onAddConfirm: (ShoppingListItemState.NewItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val listName = loadingState.data?.name ?: "Shopping List"
|
||||
|
||||
androidx.compose.material3.Scaffold(
|
||||
topBar = {
|
||||
androidx.compose.material3.TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = listName,
|
||||
style = androidx.compose.material3.MaterialTheme.typography.headlineLarge,
|
||||
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 2,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
colors = androidx.compose.material3.TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = androidx.compose.material3.MaterialTheme.colorScheme.surface,
|
||||
titleContentColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
val defaultEmptyListError = stringResource(
|
||||
R.string.shopping_list_screen_empty_list,
|
||||
loadingState.data?.name.orEmpty()
|
||||
@@ -134,7 +159,7 @@ private fun ShoppingListScreen(
|
||||
}
|
||||
|
||||
LazyColumnWithLoadingState(
|
||||
modifier = modifier,
|
||||
modifier = modifier.padding(paddingValues),
|
||||
loadingState = loadingState.map { it.items },
|
||||
emptyListError = loadingState.error?.let { getErrorMessage(it) } ?: defaultEmptyListError,
|
||||
retryButtonText = stringResource(id = R.string.shopping_lists_screen_empty_button_refresh),
|
||||
@@ -149,7 +174,6 @@ private fun ShoppingListScreen(
|
||||
onSnackbarShown = onSnackbarShown,
|
||||
onRefresh = onRefreshRequest,
|
||||
floatingActionButton = {
|
||||
// Only show the button if the editor is not active to avoid overlapping
|
||||
if (!itemBeingEdited) {
|
||||
FloatingActionButton(onClick = onAddItemClicked) {
|
||||
Icon(
|
||||
@@ -159,56 +183,70 @@ private fun ShoppingListScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
lazyListState = lazyListState
|
||||
) { sortedItems ->
|
||||
lazyListState = lazyListState,
|
||||
lazyColumnContent = { sortedItems ->
|
||||
lastAddedItemIndex = sortedItems.indexOfLast { it is ShoppingListItemState.NewItem }
|
||||
val firstCheckedItemIndex = sortedItems.indexOfFirst { it.checked }
|
||||
|
||||
lastAddedItemIndex = sortedItems.indexOfLast { it is ShoppingListItemState.NewItem }
|
||||
val firstCheckedItemIndex = sortedItems.indexOfFirst { it.checked }
|
||||
|
||||
itemsIndexed(sortedItems, { _, item -> item.id}) { index, itemState ->
|
||||
when (itemState) {
|
||||
is ShoppingListItemState.ItemLabel -> {
|
||||
ShoppingListSectionHeader(state = itemState)
|
||||
}
|
||||
is ShoppingListItemState.ExistingItem -> {
|
||||
if (itemState.isEditing) {
|
||||
val state = remember {
|
||||
ShoppingListItemEditorState(
|
||||
state = itemState,
|
||||
foods = loadingState.data?.foods.orEmpty(),
|
||||
units = loadingState.data?.units.orEmpty(),
|
||||
)
|
||||
}
|
||||
ShoppingListItemEditor(
|
||||
state = state,
|
||||
onEditCancelled = { onEditCancel(itemState) },
|
||||
onEditConfirmed = { onEditConfirm(itemState, state) },
|
||||
if (sortedItems.isNotEmpty()) {
|
||||
item(key = "hint") {
|
||||
Text(
|
||||
text = "💡 Swipe left to delete, swipe right to edit",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
modifier = Modifier.padding(
|
||||
horizontal = Dimens.Small,
|
||||
vertical = Dimens.Small
|
||||
)
|
||||
} else {
|
||||
ShoppingListItem(
|
||||
itemState = itemState,
|
||||
showDivider = firstCheckedItemIndex == index,
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||
onCheckedChange = { onItemCheckedChange(itemState, it) },
|
||||
onDismissed = { onDeleteItem(itemState) },
|
||||
onEditStart = {
|
||||
// Only allow one item to be edited at a time
|
||||
if (!itemBeingEdited) {
|
||||
onEditStart(itemState)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
is ShoppingListItemState.NewItem -> {
|
||||
ShoppingListItemEditor(
|
||||
state = itemState.item,
|
||||
onEditCancelled = { onAddCancel(itemState) },
|
||||
onEditConfirmed = { onAddConfirm(itemState) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(sortedItems, { _, item -> item.id}) { index, itemState ->
|
||||
when (itemState) {
|
||||
is ShoppingListItemState.ItemLabel -> {
|
||||
ShoppingListSectionHeader(state = itemState)
|
||||
}
|
||||
is ShoppingListItemState.ExistingItem -> {
|
||||
if (itemState.isEditing) {
|
||||
val state = remember {
|
||||
ShoppingListItemEditorState(
|
||||
state = itemState,
|
||||
foods = loadingState.data?.foods.orEmpty(),
|
||||
units = loadingState.data?.units.orEmpty(),
|
||||
)
|
||||
}
|
||||
ShoppingListItemEditor(
|
||||
state = state,
|
||||
onEditCancelled = { onEditCancel(itemState) },
|
||||
onEditConfirmed = { onEditConfirm(itemState, state) },
|
||||
)
|
||||
} else {
|
||||
ShoppingListItem(
|
||||
itemState = itemState,
|
||||
showDivider = firstCheckedItemIndex == index,
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
|
||||
onCheckedChange = { onItemCheckedChange(itemState, it) },
|
||||
onDismissed = { onDeleteItem(itemState) },
|
||||
onEditStart = {
|
||||
if (!itemBeingEdited) {
|
||||
onEditStart(itemState)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
is ShoppingListItemState.NewItem -> {
|
||||
ShoppingListItemEditor(
|
||||
state = itemState.item,
|
||||
onEditCancelled = { onAddCancel(itemState) },
|
||||
onEditConfirmed = { onAddConfirm(itemState) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,6 +645,7 @@ fun ShoppingListItem(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Checkbox(
|
||||
checked = itemState.item.checked,
|
||||
@@ -653,7 +692,6 @@ fun ShoppingListItem(
|
||||
if (!isFood) {
|
||||
appendBold(shoppingListItem.note)
|
||||
} else {
|
||||
// Add plural unit and food name if available
|
||||
shoppingListItem.unit?.let { unit ->
|
||||
appendWithPlural(unit.name, unit.pluralName,
|
||||
shoppingListItem.quantity, ::appendWithSpace)
|
||||
@@ -665,22 +703,29 @@ fun ShoppingListItem(
|
||||
}
|
||||
}
|
||||
|
||||
// only show note in secondary text if it's a food item due
|
||||
// to the note already being displayed in the primary text otherwise
|
||||
val secondaryText = shoppingListItem.takeIf { isFood }?.note.orEmpty()
|
||||
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = Dimens.Small)
|
||||
) {
|
||||
Text(
|
||||
text = primaryText,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 2,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
)
|
||||
if (secondaryText.isNotBlank()) {
|
||||
Text(
|
||||
text = secondaryText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,16 +3,20 @@ package com.atridad.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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -26,21 +30,14 @@ import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.navigate
|
||||
import com.atridad.mealient.shopping_list.R
|
||||
import com.atridad.mealient.shopping_lists.ui.composables.EditableItemBox
|
||||
import com.atridad.mealient.shopping_lists.ui.composables.getErrorMessage
|
||||
import com.atridad.mealient.shopping_lists.ui.destinations.ShoppingListScreenDestination
|
||||
import com.atridad.mealient.ui.AppTheme
|
||||
import com.atridad.mealient.ui.Dimens
|
||||
import com.atridad.mealient.ui.components.BaseScreenState
|
||||
import com.atridad.mealient.ui.components.BaseScreenWithNavigation
|
||||
import com.atridad.mealient.ui.components.LazyColumnWithLoadingState
|
||||
import com.atridad.mealient.ui.preview.ColorSchemePreview
|
||||
import com.atridad.mealient.ui.util.error
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination
|
||||
@Composable
|
||||
internal fun ShoppingListsScreen(
|
||||
navController: NavController,
|
||||
baseScreenState: BaseScreenState,
|
||||
shoppingListsViewModel: ShoppingListsViewModel = hiltViewModel(),
|
||||
) {
|
||||
val screenState by shoppingListsViewModel.shoppingListsState.collectAsState()
|
||||
@@ -50,47 +47,76 @@ internal fun ShoppingListsScreen(
|
||||
onEvent = shoppingListsViewModel::onEvent
|
||||
)
|
||||
|
||||
BaseScreenWithNavigation(
|
||||
baseScreenState = baseScreenState,
|
||||
) { modifier ->
|
||||
LazyColumnWithLoadingState(
|
||||
modifier = modifier,
|
||||
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 = 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),
|
||||
Scaffold(
|
||||
topBar = {
|
||||
androidx.compose.material3.TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Shopping Lists",
|
||||
style = androidx.compose.material3.MaterialTheme.typography.headlineLarge,
|
||||
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
) { items ->
|
||||
items(
|
||||
items = items,
|
||||
key = { it.id },
|
||||
contentType = { "Existing list" }
|
||||
) { displayList ->
|
||||
ShoppingListCard(
|
||||
listName = displayList.name,
|
||||
onClick = {
|
||||
val shoppingListId = displayList.id
|
||||
navController.navigate(ShoppingListScreenDestination(shoppingListId))
|
||||
},
|
||||
onDelete = {
|
||||
shoppingListsViewModel.onEvent(ShoppingListsEvent.RemoveList(displayList))
|
||||
},
|
||||
onEdit = {
|
||||
shoppingListsViewModel.onEvent(ShoppingListsEvent.EditList(displayList))
|
||||
}
|
||||
},
|
||||
colors = androidx.compose.material3.TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = androidx.compose.material3.MaterialTheme.colorScheme.surface,
|
||||
titleContentColor = androidx.compose.material3.MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { shoppingListsViewModel.onEvent(ShoppingListsEvent.AddShoppingList) }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringResource(id = R.string.shopping_lists_screen_add_icon_content_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
// Simple loading state
|
||||
if (screenState.loadingState is com.atridad.mealient.ui.util.LoadingStateNoData.InitialLoad) {
|
||||
Text(
|
||||
text = "Loading...",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(Dimens.Large)
|
||||
)
|
||||
} else {
|
||||
// Show shopping lists or empty state
|
||||
val shoppingLists = (screenState.loadingState as? com.atridad.mealient.ui.util.LoadingStateWithData.Success)?.data ?: emptyList()
|
||||
|
||||
if (shoppingLists.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.shopping_lists_screen_empty),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(Dimens.Large)
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
items(shoppingLists) { displayList ->
|
||||
ShoppingListCard(
|
||||
listName = displayList.name,
|
||||
onClick = {
|
||||
val shoppingListId = displayList.id
|
||||
navController.navigate(ShoppingListScreenDestination(shoppingListId))
|
||||
},
|
||||
onDelete = {
|
||||
shoppingListsViewModel.onEvent(ShoppingListsEvent.RemoveList(displayList))
|
||||
},
|
||||
onEdit = {
|
||||
shoppingListsViewModel.onEvent(ShoppingListsEvent.EditList(displayList))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,9 +135,7 @@ private fun ShoppingListsScreenDialog(
|
||||
listName = dialog.listName,
|
||||
oldName = dialog.oldListName
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
is ShoppingListsDialog.NewListItem -> {
|
||||
ShoppingListNameDialog(
|
||||
onEvent = onEvent,
|
||||
@@ -119,8 +143,6 @@ private fun ShoppingListsScreenDialog(
|
||||
listName = dialog.listName
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
is ShoppingListsDialog.RemoveListItem -> {
|
||||
DeleteListConfirmDialog(
|
||||
onEvent = onEvent,
|
||||
@@ -128,7 +150,6 @@ private fun ShoppingListsScreenDialog(
|
||||
listName = dialog.listName
|
||||
)
|
||||
}
|
||||
|
||||
is ShoppingListsDialog.None -> {
|
||||
Unit
|
||||
}
|
||||
@@ -179,8 +200,6 @@ private fun ShoppingListCard(
|
||||
imageVector = Icons.Default.ShoppingCart,
|
||||
contentDescription = stringResource(id = R.string.shopping_lists_screen_cart_icon),
|
||||
)
|
||||
|
||||
|
||||
Text(
|
||||
text = listName,
|
||||
)
|
||||
@@ -190,29 +209,3 @@ private fun ShoppingListCard(
|
||||
)
|
||||
}
|
||||
|
||||
@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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user