feat(shopping): Implement standard list loading and refined search UX
This commit is contained in:
@@ -27,6 +27,8 @@ import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.PlaylistAdd
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
import androidx.compose.material.icons.automirrored.filled.PlaylistAddCheck
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.LooksOne
|
||||
@@ -127,6 +129,7 @@ fun AppShell(
|
||||
val showCompletedItems by shoppingListsViewModel.showCompletedItems.collectAsState()
|
||||
val shoppingListWithItems by shoppingListsViewModel.getShoppingListWithItemsStream(selectedListId ?: 0)
|
||||
.collectAsState(initial = null)
|
||||
val isDetailSearchActive by shoppingListsViewModel.isDetailSearchActive.collectAsState()
|
||||
|
||||
val context = LocalContext.current
|
||||
val exportLauncher = rememberLauncherForActivityResult(
|
||||
@@ -179,6 +182,15 @@ fun AppShell(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
NavigationDrawerItem(
|
||||
label = { Text(stringResource(id = R.string.load_standard_list)) },
|
||||
selected = false,
|
||||
onClick = {
|
||||
shoppingListsViewModel.createStandardList()
|
||||
scope.launch { drawerState.close() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
@@ -192,7 +204,7 @@ fun AppShell(
|
||||
|
||||
Column {
|
||||
TopAppBar(
|
||||
title = { Text(topBarTitle) },
|
||||
title = { Text(if (currentScreen == Screen.ShoppingListDetail && isDetailSearchActive) stringResource(R.string.search) else topBarTitle) },
|
||||
navigationIcon = {
|
||||
if (isReorderMode) {
|
||||
IconButton(onClick = { shoppingListsViewModel.disableReorderMode() }) {
|
||||
@@ -202,7 +214,11 @@ fun AppShell(
|
||||
when (currentScreen) {
|
||||
is Screen.ShoppingListDetail -> {
|
||||
IconButton(onClick = {
|
||||
currentScreen = Screen.ShoppingLists; selectedListId = null
|
||||
if (isDetailSearchActive) {
|
||||
shoppingListsViewModel.toggleDetailSearch()
|
||||
} else {
|
||||
currentScreen = Screen.ShoppingLists; selectedListId = null
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
@@ -225,6 +241,9 @@ fun AppShell(
|
||||
}
|
||||
}
|
||||
if (currentScreen == Screen.ShoppingListDetail) {
|
||||
IconButton(onClick = { shoppingListsViewModel.toggleDetailSearch() }) {
|
||||
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||
}
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { showMenu = !showMenu }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = "More")
|
||||
@@ -281,7 +300,7 @@ fun AppShell(
|
||||
Icon(Icons.Filled.LooksOne, contentDescription = stringResource(R.string.add_item_simple_icon_desc))
|
||||
}
|
||||
IconButton(onClick = { shoppingListsViewModel.toggleShowCompletedItems() }) {
|
||||
Icon(imageVector = if (showCompletedItems) Icons.Filled.VisibilityOff else Icons.Filled.Visibility, contentDescription = "Toggle completed")
|
||||
Icon(imageVector = if (showCompletedItems) Icons.AutoMirrored.Filled.List else Icons.AutoMirrored.Filled.PlaylistAddCheck, contentDescription = "Toggle completed")
|
||||
}
|
||||
IconButton(onClick = { scope.launch { selectedListId?.let { shoppingListsViewModel.deleteCompletedItems(it) } } }, enabled = shoppingListWithItems?.items?.any { it.isChecked } == true) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Delete completed")
|
||||
|
||||
@@ -34,7 +34,8 @@ class AppDataContainer(private val context: Application) : AppContainer {
|
||||
override val noteshopRepository: NoteshopRepository by lazy {
|
||||
OfflineNoteshopRepository(
|
||||
noteDao = AppDatabase.getDatabase(context).noteDao(),
|
||||
shoppingListDao = AppDatabase.getDatabase(context).shoppingListDao()
|
||||
shoppingListDao = AppDatabase.getDatabase(context).shoppingListDao(),
|
||||
context = context
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ interface NoteshopRepository {
|
||||
/**
|
||||
* Insert shopping list in the data source
|
||||
*/
|
||||
suspend fun insertShoppingList(list: ShoppingList)
|
||||
suspend fun insertShoppingList(list: ShoppingList): Long
|
||||
|
||||
/**
|
||||
* Update shopping list in the data source
|
||||
@@ -86,9 +86,14 @@ interface NoteshopRepository {
|
||||
*/
|
||||
suspend fun getItemsCount(listId: Int): Int
|
||||
|
||||
/**
|
||||
* Get the standard list items from the assets.
|
||||
*/
|
||||
fun getStandardListItems(): List<String>
|
||||
|
||||
}
|
||||
|
||||
class OfflineNoteshopRepository(private val noteDao: NoteDao, private val shoppingListDao: ShoppingListDao) : NoteshopRepository {
|
||||
class OfflineNoteshopRepository(private val noteDao: NoteDao, private val shoppingListDao: ShoppingListDao, private val context: android.content.Context) : NoteshopRepository {
|
||||
override fun getAllNotesStream(): Flow<List<Note>> = noteDao.getAllNotes()
|
||||
|
||||
override fun getNoteStream(id: Int): Flow<Note?> = noteDao.getNote(id)
|
||||
@@ -103,7 +108,7 @@ class OfflineNoteshopRepository(private val noteDao: NoteDao, private val shoppi
|
||||
|
||||
override fun getShoppingListWithItemsStream(listId: Int): Flow<ShoppingListWithItems?> = shoppingListDao.getListWithItems(listId)
|
||||
|
||||
override suspend fun insertShoppingList(list: ShoppingList) = shoppingListDao.insertList(list)
|
||||
override suspend fun insertShoppingList(list: ShoppingList): Long = shoppingListDao.insertList(list)
|
||||
|
||||
override suspend fun updateShoppingList(list: ShoppingList) = shoppingListDao.updateList(list)
|
||||
|
||||
@@ -121,4 +126,8 @@ class OfflineNoteshopRepository(private val noteDao: NoteDao, private val shoppi
|
||||
|
||||
override suspend fun updateShoppingListItems(items: List<ShoppingListItem>) = shoppingListDao.updateItems(items)
|
||||
|
||||
override fun getStandardListItems(): List<String> {
|
||||
return context.resources.getStringArray(de.lxtools.noteshop.R.array.standard_list_items).toList()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@ interface ShoppingListDao {
|
||||
|
||||
// --- ShoppingList specific methods ---
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertList(list: ShoppingList)
|
||||
suspend fun insertList(list: ShoppingList): Long
|
||||
|
||||
@Update
|
||||
suspend fun updateList(list: ShoppingList)
|
||||
|
||||
@@ -67,6 +67,7 @@ fun ShoppingListDetailScreen(
|
||||
val showRenameDialog by viewModel.showRenameDialog.collectAsState()
|
||||
val showQuantityDialog by viewModel.showQuantityDialog.collectAsState()
|
||||
val isReorderMode by viewModel.isReorderMode.collectAsState()
|
||||
val isDetailSearchActive by viewModel.isDetailSearchActive.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
@@ -85,15 +86,21 @@ fun ShoppingListDetailScreen(
|
||||
OutlinedTextField(
|
||||
value = newItemName,
|
||||
onValueChange = { viewModel.onNewItemNameChange(it) },
|
||||
label = { Text(stringResource(R.string.add_item_hint)) },
|
||||
label = { Text(if (isDetailSearchActive) stringResource(R.string.search) else stringResource(R.string.add_item_hint)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
val currentSearchTerm = if (isDetailSearchActive) {
|
||||
newItemName.substringAfterLast(',').trim()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
if (newItemName.isNotBlank()) {
|
||||
val currentSearchTerm = newItemName.substringAfterLast(',').trim()
|
||||
val suggestions = if (currentSearchTerm.isNotBlank()) {
|
||||
val suggestionSearchTerm = newItemName.substringAfterLast(',').trim()
|
||||
val suggestions = if (suggestionSearchTerm.isNotBlank()) {
|
||||
shoppingListWithItems?.items?.filter {
|
||||
it.name.contains(currentSearchTerm, ignoreCase = true)
|
||||
it.name.contains(suggestionSearchTerm, ignoreCase = true)
|
||||
} ?: emptyList()
|
||||
} else {
|
||||
emptyList()
|
||||
@@ -148,20 +155,24 @@ fun ShoppingListDetailScreen(
|
||||
Text(text = stringResource(R.string.no_items_in_list))
|
||||
} else {
|
||||
val allItems = list.items.sortedBy { it.displayOrder }
|
||||
val filteredItems = if (showCompletedItems) {
|
||||
allItems
|
||||
var itemsToDisplay = if (showCompletedItems) {
|
||||
allItems.filter { it.isChecked }
|
||||
} else {
|
||||
allItems.filter { !it.isChecked }
|
||||
}
|
||||
|
||||
if (filteredItems.isEmpty() && !showCompletedItems) {
|
||||
if (isDetailSearchActive && currentSearchTerm.isNotBlank()) {
|
||||
itemsToDisplay = itemsToDisplay.filter { it.name.contains(currentSearchTerm, ignoreCase = true) }
|
||||
}
|
||||
|
||||
if (itemsToDisplay.isEmpty() && !showCompletedItems) {
|
||||
Text(text = stringResource(R.string.no_uncompleted_items))
|
||||
} else if (filteredItems.isEmpty() && showCompletedItems) {
|
||||
} else if (itemsToDisplay.isEmpty() && showCompletedItems) {
|
||||
Text(text = stringResource(R.string.no_items_in_list))
|
||||
} else {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to ->
|
||||
val reorderedItems = filteredItems.toMutableList().apply {
|
||||
val reorderedItems = itemsToDisplay.toMutableList().apply {
|
||||
add(to.index, removeAt(from.index))
|
||||
}
|
||||
viewModel.moveItem(reorderedItems)
|
||||
@@ -169,7 +180,7 @@ fun ShoppingListDetailScreen(
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
) {
|
||||
items(filteredItems, { it.id }) { item ->
|
||||
items(itemsToDisplay, { it.id }) { item ->
|
||||
ReorderableItem(reorderableLazyListState, key = item.id) { isDragging ->
|
||||
val scale = animateFloatAsState(if (isDragging) 1.05f else 1.0f, label = "scale")
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
private val _isReorderMode = MutableStateFlow(false)
|
||||
val isReorderMode: StateFlow<Boolean> = _isReorderMode.asStateFlow()
|
||||
|
||||
private val _isDetailSearchActive = MutableStateFlow(false)
|
||||
val isDetailSearchActive: StateFlow<Boolean> = _isDetailSearchActive.asStateFlow()
|
||||
|
||||
fun onNewItemNameChange(name: String) {
|
||||
_newItemName.value = name
|
||||
}
|
||||
@@ -64,6 +67,10 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
_isReorderMode.value = false
|
||||
}
|
||||
|
||||
fun toggleDetailSearch() {
|
||||
_isDetailSearchActive.value = !_isDetailSearchActive.value
|
||||
}
|
||||
|
||||
val uiState: StateFlow<ShoppingListsUiState> = combine(
|
||||
noteshopRepository.getAllShoppingListsWithItemsStream(),
|
||||
searchQuery
|
||||
@@ -283,6 +290,33 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
}
|
||||
}
|
||||
|
||||
fun createStandardList() {
|
||||
viewModelScope.launch {
|
||||
val allLists = uiState.value.shoppingLists
|
||||
val standardListName = "Standard"
|
||||
var newListName = standardListName
|
||||
var counter = 1
|
||||
while (allLists.any { it.shoppingList.name == newListName }) {
|
||||
newListName = "$standardListName ($counter)"
|
||||
counter++
|
||||
}
|
||||
|
||||
val listsCount = noteshopRepository.getListsCount()
|
||||
val newList = ShoppingList(name = newListName, displayOrder = listsCount)
|
||||
val newListId = noteshopRepository.insertShoppingList(newList)
|
||||
|
||||
val standardItems = noteshopRepository.getStandardListItems()
|
||||
standardItems.forEachIndexed { index, itemName ->
|
||||
val newItem = ShoppingListItem(
|
||||
name = itemName,
|
||||
listId = newListId.toInt(),
|
||||
displayOrder = index
|
||||
)
|
||||
noteshopRepository.insertShoppingListItem(newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatShoppingListForExport(listWithItems: ShoppingListWithItems): String {
|
||||
val builder = StringBuilder()
|
||||
builder.appendLine(listWithItems.shoppingList.name)
|
||||
|
||||
@@ -56,4 +56,69 @@
|
||||
<string name="rename_item">Element umbenennen</string>
|
||||
<string name="set_quantity">Menge festlegen</string>
|
||||
<string name="already_in_list">ist schon in der liste</string>
|
||||
<string name="load_standard_list">Standardliste laden</string>
|
||||
<string name="search">Suche</string>
|
||||
<string-array name="standard_list_items">
|
||||
<item>Brötchen</item>
|
||||
<item>Red Bull</item>
|
||||
<item>Kartoffeln</item>
|
||||
<item>Reis</item>
|
||||
<item>Karotten</item>
|
||||
<item>Brokkoli</item>
|
||||
<item>Schinken</item>
|
||||
<item>Bolognese Fix</item>
|
||||
<item>Sprudel</item>
|
||||
<item>Schweinebraten</item>
|
||||
<item>Kiwi</item>
|
||||
<item>Bananen</item>
|
||||
<item>Hamburger Fleisch</item>
|
||||
<item>Asia Nudeln</item>geo
|
||||
<item>Nudeln</item>
|
||||
<item>Wienerle</item>
|
||||
<item>Äpfel</item>
|
||||
<item>Kirschen</item>
|
||||
<item>Bockwurst</item>
|
||||
<item>Erbsen Wurzel Gemüse</item>
|
||||
<item>Milchreis</item>
|
||||
<item>Rotkohl</item>
|
||||
<item>Antikalk</item>
|
||||
<item>Apfelmus</item>
|
||||
<item>Käse</item>
|
||||
<item>Reibekäse</item>
|
||||
<item>Kaffee</item>
|
||||
<item>Salz</item>
|
||||
<item>Kräutersalz</item>
|
||||
<item>Pfeffer</item>
|
||||
<item>Pommes</item>
|
||||
<item>Pommes Salz</item>
|
||||
<item>Mehl</item>
|
||||
<item>Haferflocken</item>
|
||||
<item>Wattestäbchen</item>
|
||||
<item>Eis</item>
|
||||
<item>Nuggets</item>
|
||||
<item>Batterien</item>
|
||||
<item>Lasangne Platten</item>
|
||||
<item>Pizza Baguette</item>
|
||||
<item>Backofen Fisch</item>
|
||||
<item>Schicken Wings</item>
|
||||
<item>Wurst</item>
|
||||
<item>Gyros</item>
|
||||
<item>Zatziki</item>geo
|
||||
<item>Eier</item>
|
||||
<item>Margarine</item>
|
||||
<item>Butter</item>
|
||||
<item>Honig</item>
|
||||
<item>Eisberg Salat</item>
|
||||
<item>Frituesen Fett</item>
|
||||
<item>Rapsöl</item>
|
||||
<item>Olivenöl</item>
|
||||
<item>Wurstsalat</item>
|
||||
<item>Lorbeer Blätter</item>
|
||||
<item>Küchen Tücher</item>
|
||||
<item>Desinfektionsmittel</item>
|
||||
<item>Putzmittel</item>
|
||||
<item>Kaba</item>
|
||||
<item>Zitronentee</item>
|
||||
<item>Kidney Bohnen</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -56,4 +56,69 @@
|
||||
<string name="rename_item">Rename item</string>
|
||||
<string name="set_quantity">Set quantity</string>
|
||||
<string name="already_in_list">already in list</string>
|
||||
<string name="load_standard_list">Load standard list</string>
|
||||
<string name="search">Search</string>
|
||||
<string-array name="standard_list_items">
|
||||
<item>Bread rolls</item>
|
||||
<item>Red Bull</item>
|
||||
<item>Potatoes</item>
|
||||
<item>Rice</item>
|
||||
<item>Carrots</item>
|
||||
<item>Broccoli</item>
|
||||
<item>Ham</item>
|
||||
<item>Bolognese mix</item>
|
||||
<item>Sparkling water</item>
|
||||
<item>Roast pork</item>
|
||||
<item>Kiwi</item>
|
||||
<item>Bananas</item>
|
||||
<item>Hamburger meat</item>
|
||||
<item>Asian noodles</item>
|
||||
<item>Pasta</item>
|
||||
<item>Viennese sausages</item>
|
||||
<item>Apples</item>
|
||||
<item>Cherries</item>
|
||||
<item>Bockwurst</item>
|
||||
<item>Peas root vegetables</item>
|
||||
<item>Rice pudding</item>
|
||||
<item>Red cabbage</item>
|
||||
<item>Anti-limescale</item>
|
||||
<item>Apple sauce</item>
|
||||
<item>Cheese</item>
|
||||
<item>Grated cheese</item>
|
||||
<item>Coffee</item>
|
||||
<item>Salt</item>
|
||||
<item>Herbal salt</item>
|
||||
<item>Pepper</item>
|
||||
<item>French fries</item>
|
||||
<item>French fry salt</item>
|
||||
<item>Flour</item>
|
||||
<item>Oatmeal</item>
|
||||
<item>Cotton swabs</item>
|
||||
<item>Ice cream</item>
|
||||
<item>Nuggets</item>
|
||||
<item>Batteries</item>
|
||||
<item>Lasagna sheets</item>
|
||||
<item>Pizza baguette</item>
|
||||
<item>Baked fish</item>
|
||||
<item>Chicken wings</item>
|
||||
<item>Sausage</item>
|
||||
<item>Gyros</item>
|
||||
<item>Tzatziki</item>
|
||||
<item>Eggs</item>
|
||||
<item>Margarine</item>
|
||||
<item>Butter</item>
|
||||
<item>Honey</item>
|
||||
<item>Iceberg lettuce</item>
|
||||
<item>Deep fryer fat</item>
|
||||
<item>Canola oil</item>
|
||||
<item>Olive oil</item>
|
||||
<item>Sausage salad</item>
|
||||
<item>Bay leaves</item>
|
||||
<item>Kitchen towels</item>
|
||||
<item>Disinfectant</item>
|
||||
<item>Cleaning supplies</item>
|
||||
<item>Cocoa powder</item>
|
||||
<item>Lemon tea</item>
|
||||
<item>Kidney beans</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user