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)) Spacer(modifier = Modifier.weight(1f))
// List name // 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(
text = item.list.name, text = "$regionFlagEmoji ${item.list.name}".trim(),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = Color.White, color = Color.White,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,

View File

@ -66,6 +66,17 @@ fun ListNameImageScreen(
mutableStateOf(listData?.list?.backgroundResName) 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( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -74,6 +85,14 @@ fun ListNameImageScreen(
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back)) 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), contentPadding = PaddingValues(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.weight(1f)
) { ) {
items(allListBackgrounds) { bg -> items(allListBackgrounds) { bg ->
val isSelected = selectedBg == bg.resName 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)) 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.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn 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.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -40,23 +42,25 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.safebite.app.R import com.safebite.app.R
import com.safebite.app.presentation.screen.lists.ListsViewModel 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( private val availableRegions = listOf(
"Allemagne" to "de", Region("Allemagne", "de", "🇩🇪"),
"Australie" to "au", Region("Australie", "au", "🇦🇺"),
"Autriche" to "at", Region("Autriche", "at", "🇦🇹"),
"Canada" to "ca", Region("Canada", "ca", "🇨🇦"),
"Espagne" to "es", Region("Espagne", "es", "🇪🇸"),
"France" to "fr", Region("France", "fr", "🇫🇷"),
"Hongrie" to "hu", Region("Hongrie", "hu", "🇭🇺"),
"Italie" to "it", Region("Italie", "it", "🇮🇹"),
"Norvège" to "no", Region("Norvège", "no", "🇳🇴"),
"Pays-Bas" to "nl", Region("Pays-Bas", "nl", "🇳🇱"),
"Pologne" to "pl", Region("Pologne", "pl", "🇵🇱"),
"Portugal" to "pt", Region("Portugal", "pt", "🇵🇹"),
"Royaume-Uni" to "gb", Region("Royaume-Uni", "gb", "🇬🇧"),
"Russie" to "ru", Region("Russie", "ru", "🇷🇺"),
"Suisse (Allemand)" to "ch_de", Region("Suisse (Allemand)", "ch_de", "🇨🇭"),
"Suisse (français)" to "ch_fr" Region("Suisse (français)", "ch_fr", "🇨🇭")
) )
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -72,6 +76,13 @@ fun ListRegionScreen(
mutableStateOf(listData?.list?.region) mutableStateOf(listData?.list?.region)
} }
val onSave = {
listData?.let {
viewModel.updateList(it.list.copy(region = selectedRegion))
}
onBack()
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -80,6 +91,14 @@ fun ListRegionScreen(
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back)) 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( LazyColumn(
contentPadding = PaddingValues(vertical = 8.dp), contentPadding = PaddingValues(vertical = 8.dp),
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.weight(1f)
) { ) {
items(availableRegions) { (name, code) -> items(availableRegions) { region ->
val isSelected = selectedRegion == code val isSelected = selectedRegion == region.code
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable { selectedRegion = region.code }
selectedRegion = code
listData?.let {
// TODO: persist via viewmodel/usecase
}
}
.padding(vertical = 14.dp, horizontal = 8.dp), .padding(vertical = 14.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = name, text = "${region.flag} ${region.name}",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f) 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 { item {
val regionCode = listData.list.region
val regionSubtitle = if (regionCode != null) {
val (flag, name) = regionFlagAndName(regionCode)
"$flag $name"
} else null
SettingsTile( SettingsTile(
icon = Icons.Filled.Language, icon = Icons.Filled.Language,
label = stringResource(R.string.list_region_language), label = stringResource(R.string.list_region_language),
subtitle = regionSubtitle,
onClick = onOpenRegion 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 @Composable
private fun SettingsTile( private fun SettingsTile(
icon: androidx.compose.ui.graphics.vector.ImageVector, icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String, label: String,
subtitle: String? = null,
onClick: () -> Unit onClick: () -> Unit
) { ) {
Card( Card(
@ -238,6 +265,16 @@ private fun SettingsTile(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis 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 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.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress 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.automirrored.filled.Sort
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.DragHandle 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.Visibility
import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Card import androidx.compose.material3.Card
@ -85,6 +90,7 @@ fun ListSortScreen(
var draggedIndex by remember { mutableStateOf<Int?>(null) } var draggedIndex by remember { mutableStateOf<Int?>(null) }
var dragOffsetY by remember { mutableFloatStateOf(0f) } var dragOffsetY by remember { mutableFloatStateOf(0f) }
var previewExpanded by remember { mutableStateOf(true) }
val itemHeight = 56.dp val itemHeight = 56.dp
val itemPx = with(LocalContext.current.resources.displayMetrics) { itemHeight.value * density } val itemPx = with(LocalContext.current.resources.displayMetrics) { itemHeight.value * density }
@ -134,23 +140,22 @@ fun ListSortScreen(
modifier = Modifier.padding(vertical = 8.dp) modifier = Modifier.padding(vertical = 8.dp)
) )
// Preview card // Preview card (collapsible)
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp), .padding(vertical = 8.dp)
.clickable { previewExpanded = !previewExpanded },
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
) )
) { ) {
Column( Column(modifier = Modifier.fillMaxWidth()) {
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
@ -162,28 +167,43 @@ fun ListSortScreen(
Text( Text(
text = stringResource(R.string.list_sort_preview), text = stringResource(R.string.list_sort_preview),
style = MaterialTheme.typography.bodyMedium, 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)) AnimatedVisibility(
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { visible = previewExpanded,
orderedCategories.forEach { category -> enter = expandVertically(),
val isVisible = category in visibleCategories exit = shrinkVertically()
Row( ) {
modifier = Modifier.fillMaxWidth(), Column(
verticalAlignment = Alignment.CenterVertically modifier = Modifier
) { .fillMaxWidth()
Text( .padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
text = if (isVisible) "" else "", verticalArrangement = Arrangement.spacedBy(4.dp)
style = MaterialTheme.typography.bodySmall, ) {
color = if (isVisible) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant orderedCategories.forEach { category ->
) val isVisible = category in visibleCategories
Spacer(modifier = Modifier.width(8.dp)) Row(
Text( modifier = Modifier.fillMaxWidth(),
text = category, verticalAlignment = Alignment.CenterVertically
style = MaterialTheme.typography.bodySmall, ) {
color = if (isVisible) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant Text(
) text = if (isVisible) "" else "",
style = MaterialTheme.typography.bodySmall,
color = if (isVisible) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = category,
style = MaterialTheme.typography.bodySmall,
color = if (isVisible) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
} }
} }

View File

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