From b68212b99c793d786b8ed29c4b669212c78d094e Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sun, 26 Apr 2026 13:25:16 -0400 Subject: [PATCH] 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../data/local/database/SafeBiteDatabase.kt | 2 +- .../data/local/database/entity/Entities.kt | 1 + .../app/domain/engine/CatalogProvider.kt | 267 ++++++++++++++- .../screen/lists/IconPickerSheet.kt | 324 ++++++++++++++++++ .../screen/lists/ListDetailScreen.kt | 200 ++++++++++- .../screen/lists/ListDetailViewModel.kt | 11 +- 6 files changed, 774 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/safebite/app/presentation/screen/lists/IconPickerSheet.kt diff --git a/app/src/main/java/com/safebite/app/data/local/database/SafeBiteDatabase.kt b/app/src/main/java/com/safebite/app/data/local/database/SafeBiteDatabase.kt index a38b0b0..4e45865 100644 --- a/app/src/main/java/com/safebite/app/data/local/database/SafeBiteDatabase.kt +++ b/app/src/main/java/com/safebite/app/data/local/database/SafeBiteDatabase.kt @@ -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) diff --git a/app/src/main/java/com/safebite/app/data/local/database/entity/Entities.kt b/app/src/main/java/com/safebite/app/data/local/database/entity/Entities.kt index 046db99..8a04947 100644 --- a/app/src/main/java/com/safebite/app/data/local/database/entity/Entities.kt +++ b/app/src/main/java/com/safebite/app/data/local/database/entity/Entities.kt @@ -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() ) diff --git a/app/src/main/java/com/safebite/app/domain/engine/CatalogProvider.kt b/app/src/main/java/com/safebite/app/domain/engine/CatalogProvider.kt index 825cf50..c4f535c 100644 --- a/app/src/main/java/com/safebite/app/domain/engine/CatalogProvider.kt +++ b/app/src/main/java/com/safebite/app/domain/engine/CatalogProvider.kt @@ -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 -> "📦" } } diff --git a/app/src/main/java/com/safebite/app/presentation/screen/lists/IconPickerSheet.kt b/app/src/main/java/com/safebite/app/presentation/screen/lists/IconPickerSheet.kt new file mode 100644 index 0000000..5591b3a --- /dev/null +++ b/app/src/main/java/com/safebite/app/presentation/screen/lists/IconPickerSheet.kt @@ -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, + onDismiss: () -> Unit, + onSelectIcon: (String) -> Unit, + catalogProvider: CatalogProvider = hiltViewModel().catalog +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var searchQuery by remember { mutableStateOf("") } + val expandedCategories = remember { mutableStateMapOf() } + + 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, + 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) + ) + } + } + } +} diff --git a/app/src/main/java/com/safebite/app/presentation/screen/lists/ListDetailScreen.kt b/app/src/main/java/com/safebite/app/presentation/screen/lists/ListDetailScreen.kt index 47cb347..2045668 100644 --- a/app/src/main/java/com/safebite/app/presentation/screen/lists/ListDetailScreen.kt +++ b/app/src/main/java/com/safebite/app/presentation/screen/lists/ListDetailScreen.kt @@ -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 + ) + } + } +} diff --git a/app/src/main/java/com/safebite/app/presentation/screen/lists/ListDetailViewModel.kt b/app/src/main/java/com/safebite/app/presentation/screen/lists/ListDetailViewModel.kt index cd9f4e7..6794ebd 100644 --- a/app/src/main/java/com/safebite/app/presentation/screen/lists/ListDetailViewModel.kt +++ b/app/src/main/java/com/safebite/app/presentation/screen/lists/ListDetailViewModel.kt @@ -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 {