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:
2025-10-13 12:55:53 +02:00
parent f87aeec6d5
commit 0dae398b4d
6 changed files with 78 additions and 5 deletions

View File

@@ -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

View File

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

View File

@@ -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

View File

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

View File

@@ -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>

View File

@@ -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>