From 2cef0e399ca718ed4db60d073d559cef140cdcfc Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Tue, 28 Apr 2026 16:08:55 -0400 Subject: [PATCH] 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 --- .../presentation/screen/lists/ListsScreen.kt | 22 +++++- .../lists/settings/ListNameImageScreen.kt | 40 +++++----- .../screen/lists/settings/ListRegionScreen.kt | 70 ++++++++++------- .../lists/settings/ListSettingsScreen.kt | 37 +++++++++ .../screen/lists/settings/ListSortScreen.kt | 76 ++++++++++++------- version.properties | 4 +- 6 files changed, 174 insertions(+), 75 deletions(-) diff --git a/app/src/main/java/com/safebite/app/presentation/screen/lists/ListsScreen.kt b/app/src/main/java/com/safebite/app/presentation/screen/lists/ListsScreen.kt index 8b37d47..bf2e392 100644 --- a/app/src/main/java/com/safebite/app/presentation/screen/lists/ListsScreen.kt +++ b/app/src/main/java/com/safebite/app/presentation/screen/lists/ListsScreen.kt @@ -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, diff --git a/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListNameImageScreen.kt b/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListNameImageScreen.kt index c57a0ae..202902a 100644 --- a/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListNameImageScreen.kt +++ b/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListNameImageScreen.kt @@ -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)) } } diff --git a/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListRegionScreen.kt b/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListRegionScreen.kt index 2f06d95..07f75f5 100644 --- a/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListRegionScreen.kt +++ b/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListRegionScreen.kt @@ -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)) } } } diff --git a/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListSettingsScreen.kt b/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListSettingsScreen.kt index 5756a29..34d7718 100644 --- a/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListSettingsScreen.kt +++ b/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListSettingsScreen.kt @@ -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 = 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 + ) + } } } } diff --git a/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListSortScreen.kt b/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListSortScreen.kt index e1b820c..74ae844 100644 --- a/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListSortScreen.kt +++ b/app/src/main/java/com/safebite/app/presentation/screen/lists/settings/ListSortScreen.kt @@ -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(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( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { + Column(modifier = Modifier.fillMaxWidth()) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -162,28 +167,43 @@ 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)) { - orderedCategories.forEach { category -> - val isVisible = category in visibleCategories - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - 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 - ) + 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( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + 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 + ) + } } } } diff --git a/version.properties b/version.properties index d79e603..972bab3 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ MAJOR=1 MINOR=16 -PATCH=5 -CODE=25 +PATCH=7 +CODE=27