feat(shopping): Implement list export and improve item suggestions
Implements two new features for shopping lists:
1. **Export to Text File:**
- Adds an "Exportieren" option to the shopping list detail view menu.
- Allows users to save a shopping list as a .txt file using the Storage Access Framework.
- The exported format includes the list name, item names, quantity, and a marker for completed items (`[x]`)
2. **Improved Item Suggestions:**
- Suggestions now appear for any existing item in the list, not just completed ones.
- The suggestion text now differentiates between completed items ("(erledigt)") and items already on the list ("(ist schon in der liste)")
Also includes a minor theme adjustment to set the status and navigation bar colors.
This commit is contained in:
@@ -34,7 +34,13 @@ import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -122,6 +128,21 @@ fun AppShell(
|
||||
val shoppingListWithItems by shoppingListsViewModel.getShoppingListWithItemsStream(selectedListId ?: 0)
|
||||
.collectAsState(initial = null)
|
||||
|
||||
val context = LocalContext.current
|
||||
val exportLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("text/plain"),
|
||||
onResult = { uri ->
|
||||
uri?.let {
|
||||
shoppingListWithItems?.let { list ->
|
||||
val content = shoppingListsViewModel.formatShoppingListForExport(list)
|
||||
context.contentResolver.openOutputStream(it)?.use { outputStream ->
|
||||
outputStream.write(content.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
@@ -160,7 +181,7 @@ fun AppShell(
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)),
|
||||
topBar = {
|
||||
@@ -180,8 +201,8 @@ fun AppShell(
|
||||
} else {
|
||||
when (currentScreen) {
|
||||
is Screen.ShoppingListDetail -> {
|
||||
IconButton(onClick = {
|
||||
currentScreen = Screen.ShoppingLists; selectedListId = null
|
||||
IconButton(onClick = {
|
||||
currentScreen = Screen.ShoppingLists; selectedListId = null
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
@@ -203,6 +224,27 @@ fun AppShell(
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_shopping_list))
|
||||
}
|
||||
}
|
||||
if (currentScreen == Screen.ShoppingListDetail) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { showMenu = !showMenu }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = "More")
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Exportieren") },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
shoppingListWithItems?.let {
|
||||
val fileName = "${it.shoppingList.name}.txt"
|
||||
exportLauncher.launch(fileName)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent
|
||||
|
||||
@@ -93,7 +93,7 @@ fun ShoppingListDetailScreen(
|
||||
val currentSearchTerm = newItemName.substringAfterLast(',').trim()
|
||||
val suggestions = if (currentSearchTerm.isNotBlank()) {
|
||||
shoppingListWithItems?.items?.filter {
|
||||
it.name.contains(currentSearchTerm, ignoreCase = true) && it.isChecked
|
||||
it.name.contains(currentSearchTerm, ignoreCase = true)
|
||||
} ?: emptyList()
|
||||
} else {
|
||||
emptyList()
|
||||
@@ -126,8 +126,13 @@ fun ShoppingListDetailScreen(
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val suggestionText = if (item.isChecked) {
|
||||
stringResource(R.string.completed)
|
||||
} else {
|
||||
stringResource(R.string.already_in_list)
|
||||
}
|
||||
Text(
|
||||
text = item.name + " (" + stringResource(R.string.completed) + ")",
|
||||
text = "${item.name} ($suggestionText)",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
@@ -283,6 +283,17 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatShoppingListForExport(listWithItems: ShoppingListWithItems): String {
|
||||
val builder = StringBuilder()
|
||||
builder.appendLine(listWithItems.shoppingList.name)
|
||||
listWithItems.items.sortedBy { it.displayOrder }.forEach { item ->
|
||||
val checkedMarker = if (item.isChecked) "[x]" else "[ ]"
|
||||
val quantity = if (item.quantity != null) " (${item.quantity})" else ""
|
||||
builder.appendLine("- $checkedMarker ${item.name}$quantity")
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TIMEOUT_MILLIS = 5_000L
|
||||
|
||||
@@ -10,6 +10,10 @@ import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.core.view.WindowCompat
|
||||
import de.lxtools.noteshop.ui.theme.BluePrimary
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
@@ -51,6 +55,15 @@ fun NoteshopTheme(
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = colorScheme.primary.toArgb()
|
||||
window.navigationBarColor = colorScheme.background.toArgb()
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
|
||||
@@ -55,4 +55,5 @@
|
||||
<string name="move_item_down">Element nach unten verschieben</string>
|
||||
<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>
|
||||
</resources>
|
||||
@@ -55,4 +55,5 @@
|
||||
<string name="move_item_down">Move item down</string>
|
||||
<string name="rename_item">Rename item</string>
|
||||
<string name="set_quantity">Set quantity</string>
|
||||
<string name="already_in_list">already in list</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user