1.0.0 - Material you rework

This commit is contained in:
2025-08-31 02:24:26 -06:00
parent e4ea44f766
commit d10622c382
52 changed files with 1689 additions and 975 deletions

View File

@@ -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,
)
}
}
}
}
},

View File

@@ -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 = {}
)
}
}