feat: add custom emoji field to shopping list items, expand catalog with 200+ items across 12 categories, and implement icon picker in item detail sheet

- Add `customEmoji` field to `ShoppingListItemEntity` for user-selected icons
- Increment database version to 4
- Reorganize catalog categories: add "Condiments & Épices", "Snacks & Bonbons", "Maison & Jardin"
- Expand catalog from ~50 to 200+ items with comprehensive product coverage across all categories
- Add detail tags (Urgent, Offre, Quand cela convient) and
This commit is contained in:
Bruno Charest 2026-04-26 13:25:16 -04:00
parent a9eb582c93
commit b68212b99c
6 changed files with 774 additions and 31 deletions

View File

@ -21,7 +21,7 @@ import com.safebite.app.data.local.database.entity.UserProfileEntity
ShoppingListEntity::class,
ShoppingListItemEntity::class
],
version = 3,
version = 4,
exportSchema = false
)
@TypeConverters(Converters::class)

View File

@ -98,5 +98,6 @@ data class ShoppingListItemEntity(
val safetyStatus: String? = null, // "SAFE", "WARNING", "DANGER"
val allergenWarning: String? = null, // Allergène détecté pour alerte
val note: String? = null, // Quantité / description libre (ex: "2 kg")
val customEmoji: String? = null, // Emoji personnalisé choisi par l'utilisateur
val addedAt: Long = System.currentTimeMillis()
)

View File

@ -36,12 +36,15 @@ class CatalogProvider @Inject constructor() {
"Produits laitiers",
"Boucherie",
"Épicerie",
"Boissons",
"Condiments & Épices",
"Surgelés",
"Snacks & Bonbons",
"Boissons",
"Hygiène",
"Entretien",
"Bébé",
"Animaux"
"Animaux",
"Maison & Jardin"
)
/** Liste plate du catalogue. */
@ -66,6 +69,41 @@ class CatalogProvider @Inject constructor() {
add(CatalogItem("Pomme de terre", "Fruits & Légumes", "🥔", listOf("patate")))
add(CatalogItem("Champignon", "Fruits & Légumes", "🍄"))
add(CatalogItem("Épinard", "Fruits & Légumes", "🥬"))
add(CatalogItem("Ananas", "Fruits & Légumes", "🍍"))
add(CatalogItem("Pêche", "Fruits & Légumes", "🍑"))
add(CatalogItem("Cerise", "Fruits & Légumes", "🍒"))
add(CatalogItem("Kiwi", "Fruits & Légumes", "🥝"))
add(CatalogItem("Mangue", "Fruits & Légumes", "🥭"))
add(CatalogItem("Melon", "Fruits & Légumes", "🍈"))
add(CatalogItem("Pastèque", "Fruits & Légumes", "🍉"))
add(CatalogItem("Noix de coco", "Fruits & Légumes", "🥥"))
add(CatalogItem("Aubergine", "Fruits & Légumes", "🍆"))
add(CatalogItem("Maïs", "Fruits & Légumes", "🌽"))
add(CatalogItem("Piment", "Fruits & Légumes", "🌶️"))
add(CatalogItem("Courgette", "Fruits & Légumes", "🥒"))
add(CatalogItem("Chou-fleur", "Fruits & Légumes", "🥦"))
add(CatalogItem("Chou", "Fruits & Légumes", "🥬"))
add(CatalogItem("Betterave", "Fruits & Légumes", "🫐"))
add(CatalogItem("Radis", "Fruits & Légumes", "🥕"))
add(CatalogItem("Navet", "Fruits & Légumes", "🥕"))
add(CatalogItem("Poireau", "Fruits & Légumes", "🥬"))
add(CatalogItem("Céleri", "Fruits & Légumes", "🥬"))
add(CatalogItem("Haricots verts", "Fruits & Légumes", "🫛"))
add(CatalogItem("Petits pois", "Fruits & Légumes", "🫛"))
add(CatalogItem("Artichaut", "Fruits & Légumes", "🥬"))
add(CatalogItem("Asperge", "Fruits & Légumes", "🥬"))
add(CatalogItem("Basilic", "Fruits & Légumes", "🌿"))
add(CatalogItem("Persil", "Fruits & Légumes", "🌿"))
add(CatalogItem("Coriandre", "Fruits & Légumes", "🌿"))
add(CatalogItem("Menthe", "Fruits & Légumes", "🌿"))
add(CatalogItem("Myrtilles", "Fruits & Légumes", "🫐"))
add(CatalogItem("Framboises", "Fruits & Légumes", "🍓"))
add(CatalogItem("Mûres", "Fruits & Légumes", "🫐"))
add(CatalogItem("Abricot", "Fruits & Légumes", "🍑"))
add(CatalogItem("Prune", "Fruits & Légumes", "🍑"))
add(CatalogItem("Figue", "Fruits & Légumes", "🍇"))
add(CatalogItem("Datte", "Fruits & Légumes", "🍇"))
add(CatalogItem("Grenade", "Fruits & Légumes", "🍎"))
// Boulangerie
add(CatalogItem("Pain", "Boulangerie", "🍞", listOf("baguette")))
@ -75,16 +113,44 @@ class CatalogProvider @Inject constructor() {
add(CatalogItem("Pain de mie", "Boulangerie", "🍞"))
add(CatalogItem("Biscotte", "Boulangerie", "🍞"))
add(CatalogItem("Tortillas", "Boulangerie", "🌯"))
add(CatalogItem("Pain complet", "Boulangerie", "🍞"))
add(CatalogItem("Pain aux céréales", "Boulangerie", "🍞"))
add(CatalogItem("Pain de seigle", "Boulangerie", "🍞"))
add(CatalogItem("Bagel", "Boulangerie", "🥯"))
add(CatalogItem("Muffin", "Boulangerie", "🧁"))
add(CatalogItem("Donut", "Boulangerie", "🍩"))
add(CatalogItem("Pain au chocolat", "Boulangerie", "🥐"))
add(CatalogItem("Chausson aux pommes", "Boulangerie", "🥐"))
add(CatalogItem("Éclair", "Boulangerie", "🍰"))
add(CatalogItem("Tarte", "Boulangerie", "🥧"))
add(CatalogItem("Gâteau", "Boulangerie", "🍰"))
// Produits laitiers
add(CatalogItem("Lait", "Produits laitiers", "🥛", listOf("milk")))
add(CatalogItem("Yaourt", "Produits laitiers", "🥣", listOf("yogurt")))
add(CatalogItem("Beurre", "Produits laitiers", "🧈"))
add(CatalogItem("Fromage", "Produits laitiers", "🧀", listOf("cheese")))
add(CatalogItem("Crème", "Produits laitiers", "🥛"))
add(CatalogItem("Crème fraîche", "Produits laitiers", "🥛"))
add(CatalogItem("Œufs", "Produits laitiers", "🥚", listOf("oeufs", "eggs")))
add(CatalogItem("Mozzarella", "Produits laitiers", "🧀"))
add(CatalogItem("Parmesan", "Produits laitiers", "🧀"))
add(CatalogItem("Cheddar", "Produits laitiers", "🧀"))
add(CatalogItem("Emmental", "Produits laitiers", "🧀"))
add(CatalogItem("Camembert", "Produits laitiers", "🧀"))
add(CatalogItem("Brie", "Produits laitiers", "🧀"))
add(CatalogItem("Chèvre", "Produits laitiers", "🧀"))
add(CatalogItem("Roquefort", "Produits laitiers", "🧀"))
add(CatalogItem("Gorgonzola", "Produits laitiers", "🧀"))
add(CatalogItem("Feta", "Produits laitiers", "🧀"))
add(CatalogItem("Ricotta", "Produits laitiers", "🧀"))
add(CatalogItem("Mascarpone", "Produits laitiers", "🧀"))
add(CatalogItem("Fromage blanc", "Produits laitiers", "🥛"))
add(CatalogItem("Cottage cheese", "Produits laitiers", "🧀"))
add(CatalogItem("Crème liquide", "Produits laitiers", "🥛"))
add(CatalogItem("Lait concentré", "Produits laitiers", "🥛"))
add(CatalogItem("Lait de soja", "Produits laitiers", "🥛"))
add(CatalogItem("Lait d'amande", "Produits laitiers", "🥛"))
add(CatalogItem("Margarine", "Produits laitiers", "🧈"))
// Boucherie
add(CatalogItem("Poulet", "Boucherie", "🍗"))
@ -96,6 +162,27 @@ class CatalogProvider @Inject constructor() {
add(CatalogItem("Bacon", "Boucherie", "🥓"))
add(CatalogItem("Saumon", "Boucherie", "🐟"))
add(CatalogItem("Thon", "Boucherie", "🐟"))
add(CatalogItem("Dinde", "Boucherie", "🦃"))
add(CatalogItem("Canard", "Boucherie", "🦆"))
add(CatalogItem("Agneau", "Boucherie", "🥩"))
add(CatalogItem("Veau", "Boucherie", "🥩"))
add(CatalogItem("Côtelette", "Boucherie", "🥩"))
add(CatalogItem("Rôti", "Boucherie", "🥩"))
add(CatalogItem("Merguez", "Boucherie", "🌭"))
add(CatalogItem("Chorizo", "Boucherie", "🌭"))
add(CatalogItem("Salami", "Boucherie", "🥓"))
add(CatalogItem("Saucisson", "Boucherie", "🥓"))
add(CatalogItem("Pâté", "Boucherie", "🥓"))
add(CatalogItem("Truite", "Boucherie", "🐟"))
add(CatalogItem("Cabillaud", "Boucherie", "🐟"))
add(CatalogItem("Dorade", "Boucherie", "🐟"))
add(CatalogItem("Bar", "Boucherie", "🐟"))
add(CatalogItem("Crevettes", "Boucherie", "🦐"))
add(CatalogItem("Moules", "Boucherie", "🦪"))
add(CatalogItem("Huîtres", "Boucherie", "🦪"))
add(CatalogItem("Calamar", "Boucherie", "🦑"))
add(CatalogItem("Crabe", "Boucherie", "🦀"))
add(CatalogItem("Homard", "Boucherie", "🦞"))
// Épicerie
add(CatalogItem("Riz", "Épicerie", "🍚"))
@ -104,11 +191,9 @@ class CatalogProvider @Inject constructor() {
add(CatalogItem("Farine", "Épicerie", "🌾"))
add(CatalogItem("Sucre", "Épicerie", "🍬"))
add(CatalogItem("Sel", "Épicerie", "🧂"))
add(CatalogItem("Huile", "Épicerie", "🫒"))
add(CatalogItem("Huile d'olive", "Épicerie", "🫒"))
add(CatalogItem("Vinaigre", "Épicerie", "🧴"))
add(CatalogItem("Moutarde", "Épicerie", "🟡"))
add(CatalogItem("Ketchup", "Épicerie", "🍅"))
add(CatalogItem("Mayonnaise", "Épicerie", "🥚"))
add(CatalogItem("Confiture", "Épicerie", "🍓"))
add(CatalogItem("Miel", "Épicerie", "🍯"))
add(CatalogItem("Chocolat", "Épicerie", "🍫"))
@ -116,18 +201,56 @@ class CatalogProvider @Inject constructor() {
add(CatalogItem("Céréales", "Épicerie", "🥣", listOf("muesli")))
add(CatalogItem("Lentilles", "Épicerie", "🫘"))
add(CatalogItem("Pois chiches", "Épicerie", "🫘"))
add(CatalogItem("Haricots rouges", "Épicerie", "🫘"))
add(CatalogItem("Haricots blancs", "Épicerie", "🫘"))
add(CatalogItem("Conserves", "Épicerie", "🥫"))
add(CatalogItem("Soupe", "Épicerie", "🍲"))
add(CatalogItem("Riz basmati", "Épicerie", "🍚"))
add(CatalogItem("Quinoa", "Épicerie", "🍚"))
add(CatalogItem("Couscous", "Épicerie", "🍚"))
add(CatalogItem("Boulgour", "Épicerie", "🍚"))
add(CatalogItem("Polenta", "Épicerie", "🌽"))
add(CatalogItem("Flocons d'avoine", "Épicerie", "🥣"))
add(CatalogItem("Muesli", "Épicerie", "🥣"))
add(CatalogItem("Granola", "Épicerie", "🥣"))
add(CatalogItem("Pâte à tartiner", "Épicerie", "🍫"))
add(CatalogItem("Beurre de cacahuète", "Épicerie", "🥜"))
add(CatalogItem("Sirop d'érable", "Épicerie", "🍯"))
add(CatalogItem("Levure", "Épicerie", "🧂"))
add(CatalogItem("Bicarbonate", "Épicerie", "🧂"))
add(CatalogItem("Sucre vanillé", "Épicerie", "🍬"))
add(CatalogItem("Cacao", "Épicerie", "🍫"))
add(CatalogItem("Thon en boîte", "Épicerie", "🥫"))
add(CatalogItem("Sardines", "Épicerie", "🥫"))
add(CatalogItem("Maquereau", "Épicerie", "🥫"))
add(CatalogItem("Sauce tomate", "Épicerie", "🍅"))
add(CatalogItem("Concentré de tomate", "Épicerie", "🍅"))
add(CatalogItem("Tomates pelées", "Épicerie", "🥫"))
// Boissons
add(CatalogItem("Eau", "Boissons", "💧"))
add(CatalogItem("Jus d'orange", "Boissons", "🧃"))
add(CatalogItem("Café", "Boissons", ""))
add(CatalogItem("Thé", "Boissons", "🍵"))
add(CatalogItem("Vin", "Boissons", "🍷"))
add(CatalogItem("Bière", "Boissons", "🍺"))
add(CatalogItem("Soda", "Boissons", "🥤"))
add(CatalogItem("Limonade", "Boissons", "🍋"))
// Condiments & Épices
add(CatalogItem("Moutarde", "Condiments & Épices", "🟡"))
add(CatalogItem("Ketchup", "Condiments & Épices", "🍅"))
add(CatalogItem("Mayonnaise", "Condiments & Épices", "🥚"))
add(CatalogItem("Poivre", "Condiments & Épices", "🧂"))
add(CatalogItem("Paprika", "Condiments & Épices", "🌶️"))
add(CatalogItem("Curry", "Condiments & Épices", "🌶️"))
add(CatalogItem("Cumin", "Condiments & Épices", "🌶️"))
add(CatalogItem("Curcuma", "Condiments & Épices", "🌶️"))
add(CatalogItem("Gingembre", "Condiments & Épices", "🌶️"))
add(CatalogItem("Cannelle", "Condiments & Épices", "🌶️"))
add(CatalogItem("Muscade", "Condiments & Épices", "🌶️"))
add(CatalogItem("Thym", "Condiments & Épices", "🌿"))
add(CatalogItem("Romarin", "Condiments & Épices", "🌿"))
add(CatalogItem("Origan", "Condiments & Épices", "🌿"))
add(CatalogItem("Laurier", "Condiments & Épices", "🌿"))
add(CatalogItem("Herbes de Provence", "Condiments & Épices", "🌿"))
add(CatalogItem("Sauce soja", "Condiments & Épices", "🧴"))
add(CatalogItem("Vinaigre balsamique", "Condiments & Épices", "🧴"))
add(CatalogItem("Tabasco", "Condiments & Épices", "🌶️"))
add(CatalogItem("Harissa", "Condiments & Épices", "🌶️"))
add(CatalogItem("Wasabi", "Condiments & Épices", "🌶️"))
add(CatalogItem("Pesto", "Condiments & Épices", "🌿"))
add(CatalogItem("Tapenade", "Condiments & Épices", "🫒"))
// Surgelés
add(CatalogItem("Pizza surgelée", "Surgelés", "🍕"))
@ -135,33 +258,142 @@ class CatalogProvider @Inject constructor() {
add(CatalogItem("Glace", "Surgelés", "🍨"))
add(CatalogItem("Légumes surgelés", "Surgelés", "🥦"))
add(CatalogItem("Poisson surgelé", "Surgelés", "🐟"))
add(CatalogItem("Crevettes surgelées", "Surgelés", "🦐"))
add(CatalogItem("Fruits surgelés", "Surgelés", "🍓"))
add(CatalogItem("Plat préparé", "Surgelés", "🍱"))
add(CatalogItem("Lasagnes", "Surgelés", "🍝"))
add(CatalogItem("Nuggets", "Surgelés", "🍗"))
add(CatalogItem("Poisson pané", "Surgelés", "🐟"))
add(CatalogItem("Sorbet", "Surgelés", "🍧"))
add(CatalogItem("Gâteau glacé", "Surgelés", "🍰"))
// Snacks & Bonbons
add(CatalogItem("Chips", "Snacks & Bonbons", "🥔"))
add(CatalogItem("Cacahuètes", "Snacks & Bonbons", "🥜"))
add(CatalogItem("Noix", "Snacks & Bonbons", "🌰"))
add(CatalogItem("Amandes", "Snacks & Bonbons", "🌰"))
add(CatalogItem("Noisettes", "Snacks & Bonbons", "🌰"))
add(CatalogItem("Pistaches", "Snacks & Bonbons", "🥜"))
add(CatalogItem("Noix de cajou", "Snacks & Bonbons", "🥜"))
add(CatalogItem("Pop-corn", "Snacks & Bonbons", "🍿"))
add(CatalogItem("Bonbons", "Snacks & Bonbons", "🍬"))
add(CatalogItem("Chewing-gum", "Snacks & Bonbons", "🍬"))
add(CatalogItem("Chocolat noir", "Snacks & Bonbons", "🍫"))
add(CatalogItem("Chocolat au lait", "Snacks & Bonbons", "🍫"))
add(CatalogItem("Barres chocolatées", "Snacks & Bonbons", "🍫"))
add(CatalogItem("Barres de céréales", "Snacks & Bonbons", "🥣"))
add(CatalogItem("Fruits secs", "Snacks & Bonbons", "🍇"))
add(CatalogItem("Raisins secs", "Snacks & Bonbons", "🍇"))
// Boissons
add(CatalogItem("Eau", "Boissons", "💧"))
add(CatalogItem("Eau gazeuse", "Boissons", "💧"))
add(CatalogItem("Jus d'orange", "Boissons", "🧃"))
add(CatalogItem("Jus de pomme", "Boissons", "🧃"))
add(CatalogItem("Jus de raisin", "Boissons", "🧃"))
add(CatalogItem("Jus multivitaminé", "Boissons", "🧃"))
add(CatalogItem("Café", "Boissons", ""))
add(CatalogItem("Café moulu", "Boissons", ""))
add(CatalogItem("Café en grains", "Boissons", ""))
add(CatalogItem("Café soluble", "Boissons", ""))
add(CatalogItem("Thé", "Boissons", "🍵"))
add(CatalogItem("Thé vert", "Boissons", "🍵"))
add(CatalogItem("Thé noir", "Boissons", "🍵"))
add(CatalogItem("Infusion", "Boissons", "🍵"))
add(CatalogItem("Chocolat chaud", "Boissons", ""))
add(CatalogItem("Vin rouge", "Boissons", "🍷"))
add(CatalogItem("Vin blanc", "Boissons", "🍷"))
add(CatalogItem("Vin rosé", "Boissons", "🍷"))
add(CatalogItem("Champagne", "Boissons", "🍾"))
add(CatalogItem("Bière", "Boissons", "🍺"))
add(CatalogItem("Bière blonde", "Boissons", "🍺"))
add(CatalogItem("Bière brune", "Boissons", "🍺"))
add(CatalogItem("Cidre", "Boissons", "🍺"))
add(CatalogItem("Soda", "Boissons", "🥤"))
add(CatalogItem("Cola", "Boissons", "🥤"))
add(CatalogItem("Limonade", "Boissons", "🍋"))
add(CatalogItem("Orangina", "Boissons", "🍊"))
add(CatalogItem("Sirop", "Boissons", "🧃"))
add(CatalogItem("Smoothie", "Boissons", "🥤"))
add(CatalogItem("Boisson énergisante", "Boissons", "🥤"))
// Hygiène
add(CatalogItem("Papier toilette", "Hygiène", "🧻", listOf("toilet paper")))
add(CatalogItem("Mouchoirs", "Hygiène", "🤧"))
add(CatalogItem("Dentifrice", "Hygiène", "🦷"))
add(CatalogItem("Brosse à dents", "Hygiène", "🪥"))
add(CatalogItem("Fil dentaire", "Hygiène", "🦷"))
add(CatalogItem("Bain de bouche", "Hygiène", "🦷"))
add(CatalogItem("Shampoing", "Hygiène", "🧴"))
add(CatalogItem("Après-shampoing", "Hygiène", "🧴"))
add(CatalogItem("Savon", "Hygiène", "🧼"))
add(CatalogItem("Gel douche", "Hygiène", "🧴"))
add(CatalogItem("Déodorant", "Hygiène", "🧴"))
add(CatalogItem("Parfum", "Hygiène", "🧴"))
add(CatalogItem("Crème hydratante", "Hygiène", "🧴"))
add(CatalogItem("Crème solaire", "Hygiène", "🧴"))
add(CatalogItem("Rasoir", "Hygiène", "🪒"))
add(CatalogItem("Mousse à raser", "Hygiène", "🧴"))
add(CatalogItem("Coton-tige", "Hygiène", "🧻"))
add(CatalogItem("Coton", "Hygiène", "🧻"))
add(CatalogItem("Serviettes hygiéniques", "Hygiène", "🧻"))
add(CatalogItem("Tampons", "Hygiène", "🧻"))
// Entretien
add(CatalogItem("Lessive", "Entretien", "🧺"))
add(CatalogItem("Adoucissant", "Entretien", "🧴"))
add(CatalogItem("Liquide vaisselle", "Entretien", "🧴"))
add(CatalogItem("Tablettes lave-vaisselle", "Entretien", "🧴"))
add(CatalogItem("Éponge", "Entretien", "🧽"))
add(CatalogItem("Javel", "Entretien", "🧴"))
add(CatalogItem("Nettoyant multi-usage", "Entretien", "🧴"))
add(CatalogItem("Nettoyant vitres", "Entretien", "🧴"))
add(CatalogItem("Nettoyant sol", "Entretien", "🧴"))
add(CatalogItem("Nettoyant WC", "Entretien", "🧴"))
add(CatalogItem("Sacs poubelle", "Entretien", "🗑️"))
add(CatalogItem("Essuie-tout", "Entretien", "🧻"))
add(CatalogItem("Serpillière", "Entretien", "🧹"))
add(CatalogItem("Balai", "Entretien", "🧹"))
add(CatalogItem("Pelle", "Entretien", "🧹"))
// Bébé
add(CatalogItem("Couches", "Bébé", "👶"))
add(CatalogItem("Lait infantile", "Bébé", "🍼"))
add(CatalogItem("Compote bébé", "Bébé", "🍎"))
add(CatalogItem("Lingettes bébé", "Bébé", "🧻"))
add(CatalogItem("Petits pots", "Bébé", "🍼"))
add(CatalogItem("Céréales bébé", "Bébé", "🥣"))
add(CatalogItem("Biscuits bébé", "Bébé", "🍪"))
add(CatalogItem("Biberon", "Bébé", "🍼"))
add(CatalogItem("Tétine", "Bébé", "🍼"))
add(CatalogItem("Crème pour le change", "Bébé", "🧴"))
add(CatalogItem("Savon bébé", "Bébé", "🧼"))
add(CatalogItem("Shampoing bébé", "Bébé", "🧴"))
// Animaux
add(CatalogItem("Croquettes chien", "Animaux", "🐶"))
add(CatalogItem("Croquettes chat", "Animaux", "🐱"))
add(CatalogItem("Pâtée chien", "Animaux", "🐶"))
add(CatalogItem("Pâtée chat", "Animaux", "🐈"))
add(CatalogItem("Friandises chien", "Animaux", "🦴"))
add(CatalogItem("Friandises chat", "Animaux", "🐟"))
add(CatalogItem("Litière", "Animaux", "🐈"))
add(CatalogItem("Jouets pour animaux", "Animaux", "🎾"))
add(CatalogItem("Shampoing animal", "Animaux", "🧴"))
// Maison & Jardin
add(CatalogItem("Ampoules", "Maison & Jardin", "💡"))
add(CatalogItem("Piles", "Maison & Jardin", "🔋"))
add(CatalogItem("Allumettes", "Maison & Jardin", "🔥"))
add(CatalogItem("Bougies", "Maison & Jardin", "🕯️"))
add(CatalogItem("Papier aluminium", "Maison & Jardin", "📦"))
add(CatalogItem("Film alimentaire", "Maison & Jardin", "📦"))
add(CatalogItem("Papier cuisson", "Maison & Jardin", "📦"))
add(CatalogItem("Sacs congélation", "Maison & Jardin", "📦"))
add(CatalogItem("Terreau", "Maison & Jardin", "🌱"))
add(CatalogItem("Engrais", "Maison & Jardin", "🌱"))
add(CatalogItem("Graines", "Maison & Jardin", "🌱"))
add(CatalogItem("Pots de fleurs", "Maison & Jardin", "🪴"))
}
/** Items pour une catégorie donnée (ordre catalogue). */
@ -211,12 +443,15 @@ class CatalogProvider @Inject constructor() {
"Produits laitiers" -> "🥛"
"Boucherie" -> "🥩"
"Épicerie" -> "🛒"
"Boissons" -> "🥤"
"Condiments & Épices" -> "🌶️"
"Surgelés" -> "🧊"
"Snacks & Bonbons" -> "🍿"
"Boissons" -> "🥤"
"Hygiène" -> "🧴"
"Entretien" -> "🧹"
"Bébé" -> "👶"
"Animaux" -> "🐾"
"Maison & Jardin" -> "🏡"
else -> "📦"
}
}

View File

@ -0,0 +1,324 @@
package com.safebite.app.presentation.screen.lists
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.safebite.app.domain.engine.CatalogProvider
import javax.inject.Inject
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun IconPickerSheet(
currentEmoji: String,
categories: List<String>,
onDismiss: () -> Unit,
onSelectIcon: (String) -> Unit,
catalogProvider: CatalogProvider = hiltViewModel<ListDetailViewModel>().catalog
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var searchQuery by remember { mutableStateOf("") }
val expandedCategories = remember { mutableStateMapOf<String, Boolean>() }
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState
) {
Column(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onDismiss) {
Icon(Icons.Filled.Close, contentDescription = "Fermer")
}
Text(
text = "Choisir une icône",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
IconButton(onClick = { onSelectIcon("") }) {
Icon(
Icons.Filled.Delete,
contentDescription = "Supprimer l'icône",
tint = MaterialTheme.colorScheme.error
)
}
}
// Current icon display
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
Text(
text = currentEmoji,
style = MaterialTheme.typography.displayLarge
)
}
}
// Search bar
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
placeholder = { Text("Chercher une icône") },
leadingIcon = {
Icon(Icons.Filled.Search, contentDescription = null)
},
trailingIcon = {
if (searchQuery.isNotEmpty()) {
IconButton(onClick = { searchQuery = "" }) {
Icon(Icons.Filled.Close, contentDescription = "Effacer")
}
}
},
singleLine = true,
shape = RoundedCornerShape(28.dp)
)
// Icon grid by category
LazyColumn(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
categories.forEach { category ->
val categoryItems = catalogProvider.itemsForCategory(category)
val filteredItems = if (searchQuery.isNotBlank()) {
categoryItems.filter {
it.name.contains(searchQuery, ignoreCase = true)
}
} else {
categoryItems
}
if (filteredItems.isNotEmpty()) {
val expanded = expandedCategories[category] ?: (searchQuery.isNotBlank())
item(key = "header-$category") {
CategoryHeader(
title = category,
count = filteredItems.size,
expanded = expanded,
onToggle = {
expandedCategories[category] = !expanded
}
)
}
if (expanded) {
item(key = "grid-$category") {
IconGrid(
items = filteredItems,
currentEmoji = currentEmoji,
onSelectIcon = onSelectIcon
)
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}
@Composable
private fun CategoryHeader(
title: String,
count: Int,
expanded: Boolean,
onToggle: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable(onClick = onToggle)
.padding(vertical = 12.dp, horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.KeyboardArrowRight,
contentDescription = null,
modifier = Modifier.rotate(if (expanded) 90f else 0f),
tint = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.weight(1f)
)
Icon(
imageVector = Icons.Filled.KeyboardArrowDown,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
)
}
}
@Composable
private fun IconGrid(
items: List<CatalogProvider.CatalogItem>,
currentEmoji: String,
onSelectIcon: (String) -> Unit
) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier
.fillMaxWidth()
.height(((items.size + 2) / 3 * 100).dp.coerceAtMost(400.dp)),
contentPadding = PaddingValues(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(items) { item ->
IconCard(
emoji = item.emoji,
label = item.name,
isSelected = item.emoji == currentEmoji,
onClick = { onSelectIcon(item.emoji) }
)
}
}
}
@Composable
private fun IconCard(
emoji: String,
label: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val backgroundColor = if (isSelected) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant
}
val contentColor = if (isSelected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
Card(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.clickable(onClick = onClick),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = backgroundColor)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(8.dp)
) {
Text(
text = emoji,
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = contentColor,
textAlign = TextAlign.Center,
maxLines = 2,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal
)
}
if (isSelected) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = "Sélectionné",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(4.dp)
.size(20.dp)
)
}
}
}
}

View File

@ -260,6 +260,7 @@ fun ListDetailScreen(
onDismiss = viewModel::closeItemDetails,
onUpdateNote = { note -> viewModel.updateItemNote(selected.id, note) },
onUpdateCategory = { cat -> viewModel.updateItemCategory(selected.id, cat) },
onUpdateEmoji = { emoji -> viewModel.updateItemEmoji(selected.id, emoji) },
onMoveTo = { targetListId -> viewModel.moveItemToList(selected.id, targetListId) },
onDelete = { viewModel.deleteItem(selected.id) },
onOpenProduct = selected.barcode?.let { bc -> { onOpenProduct(bc) } }
@ -774,6 +775,7 @@ private fun ItemDetailSheet(
onDismiss: () -> Unit,
onUpdateNote: (String) -> Unit,
onUpdateCategory: (String) -> Unit,
onUpdateEmoji: (String?) -> Unit,
onMoveTo: (Long) -> Unit,
onDelete: () -> Unit,
onOpenProduct: (() -> Unit)?
@ -781,6 +783,7 @@ private fun ItemDetailSheet(
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var note by remember(item.id) { mutableStateOf(item.note.orEmpty()) }
var showCategoryPicker by remember { mutableStateOf(false) }
var showIconPicker by remember { mutableStateOf(false) }
var showMovePicker by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
@ -835,21 +838,85 @@ private fun ItemDetailSheet(
maxLines = 3
)
// Catégorie
ActionRow(
title = "Section",
value = item.category ?: "Autre",
onClick = { showCategoryPicker = true }
// Détails de l'article
Text(
text = "Détails de l'article pour ${item.productName}",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = 8.dp)
)
// Déplacer
if (otherLists.isNotEmpty()) {
ActionRow(
title = "Déplacer vers une autre liste",
value = null,
onClick = { showMovePicker = true },
leadingIcon = Icons.Filled.SwapHoriz
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
DetailTagButton(
icon = Icons.Filled.AutoAwesome,
label = "Urgent",
selected = false,
onClick = { /* TODO */ },
modifier = Modifier.weight(1f)
)
DetailTagButton(
icon = Icons.Filled.Done,
label = "Offre",
selected = false,
onClick = { /* TODO */ },
modifier = Modifier.weight(1f)
)
DetailTagButton(
icon = Icons.Filled.History,
label = "Quand cela convient",
selected = false,
onClick = { /* TODO */ },
modifier = Modifier.weight(1f)
)
}
// Paramètres
Text(
text = "Paramètres",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = 8.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
ParameterButton(
icon = Icons.Filled.Done,
label = "Changer une icône",
onClick = { showIconPicker = true },
modifier = Modifier.weight(1f)
)
ParameterButton(
icon = Icons.Filled.Camera,
label = "Ajouter une photo",
onClick = { /* TODO: Photo picker */ },
modifier = Modifier.weight(1f)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
ParameterButton(
icon = Icons.Filled.KeyboardArrowDown,
label = "Changer une section",
onClick = { showCategoryPicker = true },
modifier = Modifier.weight(1f)
)
if (otherLists.isNotEmpty()) {
ParameterButton(
icon = Icons.Filled.SwapHoriz,
label = "Déplacer l'article",
onClick = { showMovePicker = true },
modifier = Modifier.weight(1f)
)
}
}
// Ouvrir fiche produit
@ -997,6 +1064,19 @@ private fun ItemDetailSheet(
}
}
}
// Sélecteur d'icône
if (showIconPicker) {
IconPickerSheet(
currentEmoji = item.emoji,
categories = categories,
onDismiss = { showIconPicker = false },
onSelectIcon = { emoji ->
onUpdateEmoji(emoji.ifEmpty { null })
showIconPicker = false
}
)
}
}
@Composable
@ -1044,3 +1124,97 @@ private fun ActionRow(
)
}
}
@Composable
private fun DetailTagButton(
icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val backgroundColor = if (selected) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)
}
val contentColor = if (selected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
Card(
modifier = modifier
.heightIn(min = 56.dp)
.clickable(onClick = onClick),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = backgroundColor)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = contentColor,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = contentColor,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Composable
private fun ParameterButton(
icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.heightIn(min = 80.dp)
.clickable(onClick = onClick),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}

View File

@ -322,6 +322,15 @@ class ListDetailViewModel @Inject constructor(
}
}
/** Change l'emoji personnalisé d'un article. */
fun updateItemEmoji(id: Long, emoji: String?) {
viewModelScope.launch {
val listId = _listIdFlow.value
val item = manageListUseCase.getItems(listId).firstOrNull { it.id == id } ?: return@launch
manageListUseCase.updateItem(item.copy(customEmoji = emoji))
}
}
/** Déplace un article vers une autre liste. */
fun moveItemToList(id: Long, targetListId: Long) {
viewModelScope.launch {
@ -393,7 +402,7 @@ class ListDetailViewModel @Inject constructor(
safetyStatus = safetyStatus,
allergenWarning = allergenWarning,
note = note,
emoji = catalog.emojiFor(productName, category)
emoji = customEmoji ?: catalog.emojiFor(productName, category)
)
companion object {