feat: add region flag emojis to shopping lists UI, implement save actions in settings screens, add collapsible preview card in sort screen

- Display region flag emoji next to list name in `ShoppingListCard` with mapping for all supported countries
- Show region flag and name subtitle in `ListSettingsScreen` settings tile when region is set
- Replace bottom save button with top-right check icon in `ListNameImageScreen` and `ListRegionScreen`
- Add `onSave` callbacks to persist list updates via `update
This commit is contained in:
Bruno Charest 2026-04-28 16:08:55 -04:00
parent ab1bf189b3
commit 2cef0e399c
6 changed files with 174 additions and 75 deletions

View File

@ -316,8 +316,28 @@ private fun ShoppingListCard(
Spacer(modifier = Modifier.weight(1f))
// List name
val regionFlagEmoji = item.list.region?.let { code ->
when (code) {
"de" -> "🇩🇪"
"au" -> "🇦🇺"
"at" -> "🇦🇹"
"ca" -> "🇨🇦"
"es" -> "🇪🇸"
"fr" -> "🇫🇷"
"hu" -> "🇭🇺"
"it" -> "🇮🇹"
"no" -> "🇳🇴"
"nl" -> "🇳🇱"
"pl" -> "🇵🇱"
"pt" -> "🇵🇹"
"gb" -> "🇬🇧"
"ru" -> "🇷🇺"
"ch_de", "ch_fr" -> "🇨🇭"
else -> ""
}
} ?: ""
Text(
text = item.list.name,
text = "$regionFlagEmoji ${item.list.name}".trim(),
style = MaterialTheme.typography.titleLarge,
color = Color.White,
fontWeight = FontWeight.Bold,

View File

@ -66,6 +66,17 @@ fun ListNameImageScreen(
mutableStateOf(listData?.list?.backgroundResName)
}
val onSave = {
listData?.let {
val updated = it.list.copy(
name = listName.ifBlank { it.list.name },
backgroundResName = selectedBg
)
viewModel.updateList(updated)
}
onBack()
}
Scaffold(
topBar = {
TopAppBar(
@ -74,6 +85,14 @@ fun ListNameImageScreen(
IconButton(onClick = onBack) {
Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
}
},
actions = {
IconButton(onClick = onSave) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = stringResource(R.string.action_save)
)
}
}
)
}
@ -153,7 +172,9 @@ fun ListNameImageScreen(
contentPadding = PaddingValues(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
items(allListBackgrounds) { bg ->
val isSelected = selectedBg == bg.resName
@ -199,23 +220,6 @@ fun ListNameImageScreen(
}
}
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = {
val updated = listData.list.copy(
name = listName.ifBlank { listData.list.name },
backgroundResName = selectedBg
)
// TODO: update via viewmodel/usecase
onBack()
},
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
) {
Text(stringResource(R.string.action_save))
}
Spacer(modifier = Modifier.height(16.dp))
}
}

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@ -17,6 +18,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -40,23 +42,25 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.safebite.app.R
import com.safebite.app.presentation.screen.lists.ListsViewModel
private data class Region(val name: String, val code: String, val flag: String)
private val availableRegions = listOf(
"Allemagne" to "de",
"Australie" to "au",
"Autriche" to "at",
"Canada" to "ca",
"Espagne" to "es",
"France" to "fr",
"Hongrie" to "hu",
"Italie" to "it",
"Norvège" to "no",
"Pays-Bas" to "nl",
"Pologne" to "pl",
"Portugal" to "pt",
"Royaume-Uni" to "gb",
"Russie" to "ru",
"Suisse (Allemand)" to "ch_de",
"Suisse (français)" to "ch_fr"
Region("Allemagne", "de", "🇩🇪"),
Region("Australie", "au", "🇦🇺"),
Region("Autriche", "at", "🇦🇹"),
Region("Canada", "ca", "🇨🇦"),
Region("Espagne", "es", "🇪🇸"),
Region("France", "fr", "🇫🇷"),
Region("Hongrie", "hu", "🇭🇺"),
Region("Italie", "it", "🇮🇹"),
Region("Norvège", "no", "🇳🇴"),
Region("Pays-Bas", "nl", "🇳🇱"),
Region("Pologne", "pl", "🇵🇱"),
Region("Portugal", "pt", "🇵🇹"),
Region("Royaume-Uni", "gb", "🇬🇧"),
Region("Russie", "ru", "🇷🇺"),
Region("Suisse (Allemand)", "ch_de", "🇨🇭"),
Region("Suisse (français)", "ch_fr", "🇨🇭")
)
@OptIn(ExperimentalMaterial3Api::class)
@ -72,6 +76,13 @@ fun ListRegionScreen(
mutableStateOf(listData?.list?.region)
}
val onSave = {
listData?.let {
viewModel.updateList(it.list.copy(region = selectedRegion))
}
onBack()
}
Scaffold(
topBar = {
TopAppBar(
@ -80,6 +91,14 @@ fun ListRegionScreen(
IconButton(onClick = onBack) {
Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
}
},
actions = {
IconButton(onClick = onSave) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = stringResource(R.string.action_save)
)
}
}
)
}
@ -99,24 +118,21 @@ fun ListRegionScreen(
LazyColumn(
contentPadding = PaddingValues(vertical = 8.dp),
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
items(availableRegions) { (name, code) ->
val isSelected = selectedRegion == code
items(availableRegions) { region ->
val isSelected = selectedRegion == region.code
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
selectedRegion = code
listData?.let {
// TODO: persist via viewmodel/usecase
}
}
.clickable { selectedRegion = region.code }
.padding(vertical = 14.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = name,
text = "${region.flag} ${region.name}",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f)
)
@ -139,6 +155,8 @@ fun ListRegionScreen(
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}

View File

@ -154,9 +154,15 @@ fun ListSettingsScreen(
)
}
item {
val regionCode = listData.list.region
val regionSubtitle = if (regionCode != null) {
val (flag, name) = regionFlagAndName(regionCode)
"$flag $name"
} else null
SettingsTile(
icon = Icons.Filled.Language,
label = stringResource(R.string.list_region_language),
subtitle = regionSubtitle,
onClick = onOpenRegion
)
}
@ -201,10 +207,31 @@ fun ListSettingsScreen(
}
}
private fun regionFlagAndName(code: String): Pair<String, String> = when (code) {
"de" -> "🇩🇪" to "Allemagne"
"au" -> "🇦🇺" to "Australie"
"at" -> "🇦🇹" to "Autriche"
"ca" -> "🇨🇦" to "Canada"
"es" -> "🇪🇸" to "Espagne"
"fr" -> "🇫🇷" to "France"
"hu" -> "🇭🇺" to "Hongrie"
"it" -> "🇮🇹" to "Italie"
"no" -> "🇳🇴" to "Norvège"
"nl" -> "🇳🇱" to "Pays-Bas"
"pl" -> "🇵🇱" to "Pologne"
"pt" -> "🇵🇹" to "Portugal"
"gb" -> "🇬🇧" to "Royaume-Uni"
"ru" -> "🇷🇺" to "Russie"
"ch_de" -> "🇨🇭" to "Suisse (Allemand)"
"ch_fr" -> "🇨🇭" to "Suisse (français)"
else -> "" to code
}
@Composable
private fun SettingsTile(
icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String,
subtitle: String? = null,
onClick: () -> Unit
) {
Card(
@ -238,6 +265,16 @@ private fun SettingsTile(
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (subtitle != null) {
Spacer(modifier = Modifier.height(2.dp))
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}

View File

@ -1,5 +1,8 @@
package com.safebite.app.presentation.screen.lists.settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
@ -23,6 +26,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Sort
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Card
@ -85,6 +90,7 @@ fun ListSortScreen(
var draggedIndex by remember { mutableStateOf<Int?>(null) }
var dragOffsetY by remember { mutableFloatStateOf(0f) }
var previewExpanded by remember { mutableStateOf(true) }
val itemHeight = 56.dp
val itemPx = with(LocalContext.current.resources.displayMetrics) { itemHeight.value * density }
@ -134,23 +140,22 @@ fun ListSortScreen(
modifier = Modifier.padding(vertical = 8.dp)
)
// Preview card
// Preview card (collapsible)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
.padding(vertical = 8.dp)
.clickable { previewExpanded = !previewExpanded },
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
Column(
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@ -162,11 +167,25 @@ fun ListSortScreen(
Text(
text = stringResource(R.string.list_sort_preview),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.SemiBold,
modifier = Modifier.weight(1f)
)
Icon(
imageVector = if (previewExpanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown,
contentDescription = if (previewExpanded) "Réduire" else "Développer"
)
}
Spacer(modifier = Modifier.height(8.dp))
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
AnimatedVisibility(
visible = previewExpanded,
enter = expandVertically(),
exit = shrinkVertically()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
orderedCategories.forEach { category ->
val isVisible = category in visibleCategories
Row(
@ -189,6 +208,7 @@ fun ListSortScreen(
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider()

View File

@ -1,4 +1,4 @@
MAJOR=1
MINOR=16
PATCH=5
CODE=25
PATCH=7
CODE=27