diff --git a/CHANGELOG.md b/CHANGELOG.md index 8469e78..a201897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to EverShelf will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.8] - 2026-05-10 + +### Added +- **Usa ingredienti dalla chat** — Quando la chat con Gemini Chef genera una ricetta, compare il bottone "🥄 Usa ingredienti" sotto la risposta. Premendolo, il backend analizza la ricetta e abbina gli ingredienti all'inventario tramite fuzzy matching (stessa logica della sezione Ricette). Vengono mostrati i pulsanti "📦 Usa" per ogni ingrediente disponibile in dispensa, con posizione e scadenza. Il flusso di scalatura inventario è identico a quello della sezione Ricette. + ## [1.7.7] - 2026-05-10 ### Fixed diff --git a/api/index.php b/api/index.php index ad8f9ae..ae99467 100644 --- a/api/index.php +++ b/api/index.php @@ -111,7 +111,7 @@ function checkRateLimit(string $action): void { } // Determine limit based on action - $aiActions = ['gemini_readExpiry', 'gemini_chat', 'gemini_identify', 'gemini_suggest_shopping']; + $aiActions = ['gemini_readExpiry', 'gemini_chat', 'gemini_identify', 'gemini_suggest_shopping', 'chat_extract_recipe']; $loginActions = []; $recipeActions = ['generate_recipe', 'generate_recipe_stream']; $errorActions = ['report_error', 'check_update']; @@ -335,6 +335,10 @@ try { geminiChat($db); break; + case 'chat_extract_recipe': + chatExtractRecipe($db); + break; + // ===== BRING! SHOPPING LIST ===== case 'bring_list': bringGetList(); @@ -3591,6 +3595,232 @@ PROMPT; } } +// ===== CHAT: EXTRACT RECIPE INGREDIENTS ===== +function chatExtractRecipe(PDO $db): void { + $apiKey = env('GEMINI_API_KEY'); + if (empty($apiKey)) { + echo json_encode(['success' => false, 'error' => 'no_api_key']); + return; + } + + $input = json_decode(file_get_contents('php://input'), true); + $replyText = trim($input['text'] ?? ''); + + if (empty($replyText)) { + echo json_encode(['success' => false, 'error' => 'empty_text']); + return; + } + + // Fetch full inventory — same query as generateRecipe + $stmt = $db->query(" + SELECT p.id AS product_id, p.name, p.brand, p.category, i.quantity, p.unit, p.default_quantity, p.package_unit, i.location, i.expiry_date, i.opened_at, + CASE WHEN i.expiry_date IS NOT NULL THEN julianday(i.expiry_date) - julianday('now') ELSE 999 END AS days_left + FROM inventory i + JOIN products p ON p.id = i.product_id + WHERE i.quantity > 0 + ORDER BY days_left ASC + "); + $items = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // --- Gemini call: extract ingredient list only (no inventory in prompt → tiny output) --- + // The PHP fuzzy-matching below does all the inventory matching, exactly like generateRecipe. + $prompt = "Extract all ingredients from the recipe text below.\nReturn ONLY a compact JSON array — no markdown, no extra text.\nEach element: {\"name\":\"...\",\"qty\":\"...\",\"qty_number\":0.0,\"unit\":\"g|ml|pz|conf|kg|l\"}\n\nRECIPE:\n{$replyText}"; + + $payload = [ + 'contents' => [['role' => 'user', 'parts' => [['text' => $prompt]]]], + 'generationConfig' => ['temperature' => 0.1, 'maxOutputTokens' => 2048] + ]; + + $result = callGeminiWithFallback($apiKey, $payload, 30); + + if ($result['http_code'] !== 200) { + echo json_encode(['success' => false, 'error' => $result['data']['error']['message'] ?? 'gemini_error']); + return; + } + + $text = $result['data']['candidates'][0]['content']['parts'][0]['text'] ?? ''; + if (empty($text)) { + echo json_encode(['success' => false, 'error' => 'gemini_error']); + return; + } + + // Strip markdown code fences if present + $text = preg_replace('/^```(?:json)?\s*/i', '', $text); + $text = preg_replace('/\s*```$/i', '', $text); + $text = trim($text); + + $ingredients = json_decode($text, true); + if (!is_array($ingredients) || empty($ingredients)) { + echo json_encode(['success' => false, 'error' => 'parse_error', 'raw' => mb_substr($text, 0, 500)]); + return; + } + + // Mark all extracted ingredients as from_pantry=true so the enrichment logic tries to match them all + foreach ($ingredients as &$ing) { + $ing['from_pantry'] = true; + } + unset($ing); + + // PHP fuzzy-match against full inventory — same logic as generateRecipe + _enrichChatIngredients($ingredients, $items); + + // Extract recipe title from the reply text (look for bold title at start) + $recipeTitle = ''; + if (preg_match('/\*\*([^*\n]{3,60})\*\*/u', $replyText, $m)) { + $recipeTitle = trim($m[1]); + } + + echo json_encode(['success' => true, 'ingredients' => $ingredients, 'title' => $recipeTitle]); +} + +function _enrichChatIngredients(array &$ingredients, array $items): void { + if (empty($ingredients) || empty($items)) return; + + // Build lookup + $itemsLookup = []; + foreach ($items as $item) { + $itemsLookup[] = [ + 'item' => $item, + 'lower' => mb_strtolower(trim($item['name']), 'UTF-8'), + 'words' => preg_split('/[\s,.\-\/]+/', mb_strtolower(trim($item['name']), 'UTF-8')), + ]; + } + + $aliases = [ + 'uovo' => ['uova','uovo','egg'], + 'uova' => ['uovo','uova','egg'], + 'latte' => ['latte','milk'], + 'formaggio' => ['formaggio','cheese','philadelphia','mozzarella','parmigiano','grana','pecorino','ricotta','mascarpone','stracchino','gorgonzola'], + 'pasta' => ['pasta','spaghetti','penne','fusilli','rigatoni','farfalle','tagliatelle','linguine','bucatini','orecchiette','paccheri','maccheroni'], + 'pomodoro' => ['pomodoro','pomodori','tomato','passata','pelati','polpa'], + 'cipolla' => ['cipolla','cipolle','onion'], + 'aglio' => ['aglio','garlic'], + 'burro' => ['burro','butter'], + 'panna' => ['panna','cream','crema'], + 'zucchero' => ['zucchero','sugar'], + 'farina' => ['farina','flour'], + 'olio' => ['olio','oil'], + 'patata' => ['patata','patate','potato'], + 'carota' => ['carota','carote','carrot'], + 'sedano' => ['sedano','celery'], + 'prezzemolo' => ['prezzemolo','parsley'], + 'basilico' => ['basilico','basil'], + ]; + + foreach ($ingredients as &$ing) { + // Try to match ALL ingredients — from_pantry was set to true for all by chatExtractRecipe + // If no match is found, product_id stays unset → shown as 🛒 in frontend + + $ingNameLower = mb_strtolower(trim($ing['name']), 'UTF-8'); + $ingWords = preg_split('/[\s,.\-\/]+/', $ingNameLower); + $bestMatch = null; + $bestScore = 0; + + foreach ($itemsLookup as $entry) { + $itemNameLower = $entry['lower']; + $itemWords = $entry['words']; + $score = 0; + + if ($ingNameLower === $itemNameLower) { + $score = 100; + } elseif (mb_strpos($itemNameLower, $ingNameLower) !== false) { + $score = 80; + } elseif (mb_strpos($ingNameLower, $itemNameLower) !== false) { + $score = 70; + } else { + $expandedIngWords = $ingWords; + foreach ($ingWords as $w) { + foreach ($aliases as $key => $group) { + if (in_array($w, $group) || mb_strpos($w, $key) === 0 || mb_strpos($key, $w) === 0) { + $expandedIngWords = array_merge($expandedIngWords, $group); + } + } + } + $expandedIngWords = array_unique($expandedIngWords); + $common = 0; + foreach ($expandedIngWords as $ew) { + foreach ($itemWords as $iw) { + $minLen = min(mb_strlen($ew), mb_strlen($iw)); + if ($minLen >= 3) { + $prefixLen = 0; + for ($c = 0; $c < $minLen; $c++) { + if (mb_substr($ew, $c, 1) === mb_substr($iw, $c, 1)) $prefixLen++; + else break; + } + if ($prefixLen >= min(4, $minLen)) { $common++; break; } + } + if ($ew === $iw) { $common++; break; } + } + } + if ($common > 0) { + $score = ($common / max(count($ingWords), 1)) * 65; + if (count($ingWords) > 0) { + foreach ($itemWords as $iw) { + if (mb_strpos($iw, $ingWords[0]) === 0 || mb_strpos($ingWords[0], $iw) === 0) { + $score += 10; break; + } + } + } + } + } + + if ($score > $bestScore) { + $bestScore = $score; + $bestMatch = $entry['item']; + } + } + + if ($bestMatch && $bestScore > 30) { + $ing['product_id'] = (int)$bestMatch['product_id']; + $ing['location'] = $bestMatch['location']; + $ing['inventory_unit'] = $bestMatch['unit']; + $ing['inventory_qty'] = (float)$bestMatch['quantity']; + $ing['default_quantity'] = (float)($bestMatch['default_quantity'] ?? 0); + $ing['package_unit'] = $bestMatch['package_unit'] ?? ''; + $ing['available_qty'] = $bestMatch['quantity'] . ' ' . $bestMatch['unit']; + $ing['vacuum_sealed'] = !empty($bestMatch['vacuum_sealed']) ? 1 : 0; + if (!empty($bestMatch['brand'])) $ing['brand'] = $bestMatch['brand']; + if (!empty($bestMatch['expiry_date'])) $ing['expiry_date'] = $bestMatch['expiry_date']; + + // Validate and convert qty_number to inventory unit + $qtyNum = (float)($ing['qty_number'] ?? 0); + $invUnit = $bestMatch['unit'] ?? 'pz'; + $invQty = (float)$bestMatch['quantity']; + + if ($qtyNum > 0) { + $recipeQty = $ing['qty'] ?? ''; + $recipeUnit = ''; + $recipeVal = 0; + if (preg_match('/(\d+[.,]?\d*)\s*(g|gr|gramm|kg|ml|l|litri|cl|pz|pezz|conf)/i', $recipeQty, $qm)) { + $recipeVal = (float)str_replace(',', '.', $qm[1]); + $ru = strtolower($qm[2]); + if (strpos($ru, 'g') === 0) $recipeUnit = 'g'; + elseif ($ru === 'kg') { $recipeUnit = 'g'; $recipeVal *= 1000; } + elseif ($ru === 'ml') $recipeUnit = 'ml'; + elseif ($ru === 'cl') { $recipeUnit = 'ml'; $recipeVal *= 10; } + elseif ($ru === 'l' || strpos($ru, 'litr') === 0) { $recipeUnit = 'ml'; $recipeVal *= 1000; } + elseif (strpos($ru, 'pz') === 0 || strpos($ru, 'pezz') === 0) $recipeUnit = 'pz'; + elseif (strpos($ru, 'conf') === 0) $recipeUnit = 'conf'; + } + if ($recipeUnit && $recipeUnit !== $invUnit) { + if ($recipeUnit === 'g' && $invUnit === 'g') $qtyNum = $recipeVal; + elseif ($recipeUnit === 'g' && $invUnit === 'kg') $qtyNum = $recipeVal / 1000; + elseif ($recipeUnit === 'ml' && $invUnit === 'ml') $qtyNum = $recipeVal; + elseif ($recipeUnit === 'ml' && $invUnit === 'l') $qtyNum = $recipeVal / 1000; + elseif ($invUnit === 'pz' || $invUnit === 'conf') { + $defQty = (float)($bestMatch['default_quantity'] ?? 0); + $qtyNum = $defQty > 0 ? max(0.25, round(($recipeVal / $defQty) * 4) / 4) : max(1, round($recipeVal / 100)); + } + } + if ($qtyNum > $invQty) $qtyNum = $invQty; + if ($recipeVal > 0 && $recipeUnit === $invUnit && $qtyNum < $recipeVal * 0.01) $qtyNum = $recipeVal; + $ing['qty_number'] = round($qtyNum, 3); + } + } + } + unset($ing); +} + // ===== RECIPE GENERATION — STREAMING AGENT ===== function generateRecipeStream(PDO $db): void { // Override content-type for SSE before any output is sent diff --git a/assets/css/style.css b/assets/css/style.css index ecfead2..28e7e39 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -5630,6 +5630,51 @@ body.cooking-mode-active .app-header { 30% { transform: translateY(-6px); opacity: 1; } } +/* ====== Chat Recipe Ingredient Panel ====== */ +.btn-chat-use-recipe { + display: inline-block; + margin-top: 10px; + padding: 7px 14px; + background: var(--card-bg, #1e293b); + border: 1px solid #6366f1; + color: #a5b4fc; + border-radius: 20px; + font-size: 0.82rem; + cursor: pointer; + transition: all 0.15s; +} +.btn-chat-use-recipe:hover { + background: #312e81; + color: white; +} +.btn-chat-use-recipe:disabled { + opacity: 0.6; + cursor: default; +} + +.chat-recipe-panel-container { + margin-top: 8px; +} + +.chat-recipe-panel { + background: var(--card-bg, #1e293b); + border: 1px solid rgba(99,102,241,0.3); + border-radius: 12px; + padding: 12px 14px; + margin-top: 4px; +} + +.chat-recipe-panel-title { + font-size: 0.9rem; + margin-bottom: 4px; +} + +.chat-recipe-panel-subtitle { + font-size: 0.75rem; + color: var(--text-muted, #94a3b8); + margin-bottom: 10px; +} + /* ====== Recipe Archive ====== */ .recipe-archive { display: flex; diff --git a/assets/js/app.js b/assets/js/app.js index da59f4d..2b97a15 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -11355,7 +11355,7 @@ async function submitRecipeUse(useAll) { btn.textContent = '⏳...'; try { - const recipeTitle = _cachedRecipe?.recipe?.title || ''; + const recipeTitle = _cachedRecipe?.recipe?.title || _chatRecipeTitle || ''; const result = await api('inventory_use', {}, 'POST', { product_id: productId, quantity: qty, @@ -12382,6 +12382,7 @@ async function generateRecipe() { let chatHistory = []; let chatInventoryContext = null; let _chatSavedCount = 0; // track how many messages already saved to DB +let _chatRecipeTitle = ''; // title of last recipe extracted from chat (used in confirmRecipeUse notes) function initChat() { // Load chat history from DB @@ -12415,6 +12416,89 @@ function sendChatSuggestion(text) { sendChatMessage(); } +/** Returns true if a chat reply looks like it contains a recipe with ingredients */ +function _looksLikeRecipe(text) { + // Must have an "Ingredienti" section header AND a step/preparation section + const hasIngredients = /ingredi[e|ë]nti/i.test(text); + const hasPreparation = /preparazi[o|ó]ne|procedimento|istruzioni|passaggi|how to|steps|zubereitung/i.test(text); + const hasStepNumbers = /^\d+[\.\)]/m.test(text); + return hasIngredients && (hasPreparation || hasStepNumbers); +} + +async function chatExtractIngredients(btn, replyText) { + btn.disabled = true; + btn.textContent = '⏳ Analisi in corso...'; + + const panel = btn.nextElementSibling; + + try { + const settings = getSettings(); + const result = await api('chat_extract_recipe', {}, 'POST', { + text: replyText, + lang: settings.lang || 'it' + }); + + if (!result.success || !result.ingredients) { + btn.textContent = '⚠️ ' + (result.error || t('error.generic')); + btn.disabled = false; + return; + } + + const matchedIngredients = result.ingredients.filter(i => i.product_id); + if (matchedIngredients.length === 0) { + btn.textContent = '📦 Nessun ingrediente trovato in dispensa'; + return; + } + + _chatRecipeTitle = result.title || ''; + + // Render inline ingredient panel + btn.style.display = 'none'; + panel.innerHTML = _buildChatIngredientPanelHTML(result.ingredients, result.title); + panel.style.display = 'block'; + + } catch(err) { + btn.textContent = '⚠️ ' + t('error.connection'); + btn.disabled = false; + } +} + +function _buildChatIngredientPanelHTML(ingredients, title) { + let html = `
`; + if (title) html += `
🍽️ ${escapeHtml(title)}
`; + html += `
${t('chat.recipe_ingredients_from_pantry') || 'Ingredienti in dispensa'}
`; + html += `
`; + return html; +} + async function sendChatMessage() { const input = document.getElementById('chat-input'); const text = input.value.trim(); @@ -12453,7 +12537,20 @@ async function sendChatMessage() { if (result.success) { chatHistory.push({ role: 'gemini', text: result.reply }); - appendChatBubble('gemini', formatChatReply(result.reply)); + const bubble = appendChatBubble('gemini', formatChatReply(result.reply)); + // If reply looks like a recipe, append "Usa ingredienti" button + if (_looksLikeRecipe(result.reply)) { + const replyText = result.reply; + const useBtn = document.createElement('button'); + useBtn.className = 'btn-chat-use-recipe'; + useBtn.textContent = '🥄 ' + (t('chat.use_ingredients_btn') || 'Usa ingredienti'); + useBtn.onclick = () => chatExtractIngredients(useBtn, replyText); + const panelDiv = document.createElement('div'); + panelDiv.className = 'chat-recipe-panel-container'; + panelDiv.style.display = 'none'; + bubble.appendChild(useBtn); + bubble.appendChild(panelDiv); + } } else { const errMsg = result.error === 'no_api_key' ? 'Configura la chiave API Gemini nelle impostazioni.' : (result.error || 'Errore nella risposta'); appendChatBubble('gemini', `⚠️ ${escapeHtml(errMsg)}`); diff --git a/data/anomaly_dismissed.json b/data/anomaly_dismissed.json index 2ecf9fa..0967ef4 100644 --- a/data/anomaly_dismissed.json +++ b/data/anomaly_dismissed.json @@ -1 +1 @@ -{"a_32_572":1776776330,"a_17_171":1776776404,"a_25_-777":1776776427,"a_7_-279":1776776434,"a_168_253":1777223925,"a_191_1":1777300414,"a_183_291":1777310462,"a_213_150":1777378506,"a_183_290":1777380000} \ No newline at end of file +{} diff --git a/data/opened_shelf_cache.json b/data/opened_shelf_cache.json index 6db3b20..a22338d 100644 --- a/data/opened_shelf_cache.json +++ b/data/opened_shelf_cache.json @@ -1,51 +1,156 @@ { - "226887def70e33ef73290ebfe75ed4d0": { - "days": 7, - "source": "ai", - "name": "Polpa di pomodoro finissima", - "location": "frigo", - "ts": 1777444819 - }, - "0ed51c9496aa9edfe38caf41772f54ed": { - "days": 7, - "source": "rule", - "name": "Latte di Montagna", - "location": "frigo", - "ts": 1777444820 - }, - "2d63d0216a75d46b465150e925d2e7ad": { - "days": 30, - "source": "rule", - "name": "Burro", - "location": "frigo", - "ts": 1777444821 - }, - "9afdf35c4a256867ef47c32495349eb6": { - "days": 5, - "source": "rule", - "name": "Yaourt Vanille", - "location": "frigo", - "ts": 1777480477 - }, - "584f57418733a1f2acd29fe2e8816129": { - "days": 5, - "source": "rule", - "name": "Passata di pomodoro", - "location": "frigo", - "ts": 1778133522 - }, - "baeb7f2021b4bb91c368c9131a61f07c": { - "days": 10, - "source": "rule", - "name": "Formaggio Monte Maria", - "location": "frigo", - "ts": 1778133523 - }, - "063f2d534407214786d039bb2bffbb93": { - "days": 5, - "source": "rule", - "name": "Carote", - "location": "frigo", - "ts": 1778133524 - } + "226887def70e33ef73290ebfe75ed4d0": { + "days": 7, + "source": "ai", + "name": "Polpa di pomodoro finissima", + "location": "frigo", + "ts": 1777444819 + }, + "0ed51c9496aa9edfe38caf41772f54ed": { + "days": 7, + "source": "rule", + "name": "Latte di Montagna", + "location": "frigo", + "ts": 1777444820 + }, + "2d63d0216a75d46b465150e925d2e7ad": { + "days": 30, + "source": "rule", + "name": "Burro", + "location": "frigo", + "ts": 1777444821 + }, + "9afdf35c4a256867ef47c32495349eb6": { + "days": 5, + "source": "rule", + "name": "Yaourt Vanille", + "location": "frigo", + "ts": 1777480477 + }, + "584f57418733a1f2acd29fe2e8816129": { + "days": 5, + "source": "rule", + "name": "Passata di pomodoro", + "location": "frigo", + "ts": 1778133522 + }, + "baeb7f2021b4bb91c368c9131a61f07c": { + "days": 10, + "source": "rule", + "name": "Formaggio Monte Maria", + "location": "frigo", + "ts": 1778133523 + }, + "063f2d534407214786d039bb2bffbb93": { + "days": 5, + "source": "rule", + "name": "Carote", + "location": "frigo", + "ts": 1778133524 + }, + "10a3d07c19bb1f889ebc9293862b4b36": { + "days": 60, + "source": "rule", + "name": "Ovomaltine", + "location": "dispensa", + "ts": 1778419084 + }, + "0fbad7ccd8b6155c06aaa6b3c17a67d3": { + "days": 365, + "source": "rule", + "name": "Linguine pasta di Gragnano Igp", + "location": "dispensa", + "ts": 1778419084 + }, + "b4a03e7356e7a0983b9c8af5f3cd8c57": { + "days": 60, + "source": "rule", + "name": "Polpa di pomodoro finissima", + "location": "dispensa", + "ts": 1778419085 + }, + "b8334ff0febd5c0440c9b24c9f3132ed": { + "days": 180, + "source": "rule", + "name": "Basilico tritato surgelato", + "location": "freezer", + "ts": 1778419086 + }, + "0cb14384d0ba763ccf12e079d6aa8d34": { + "days": 60, + "source": "rule", + "name": "Salsa Pronta Ciliegini", + "location": "dispensa", + "ts": 1778419086 + }, + "188634f49edb8b014a46942ee9fad689": { + "days": 180, + "source": "rule", + "name": "Farina Barilla", + "location": "dispensa", + "ts": 1778419204 + }, + "c8db359d8709c69a95f0e6f68216d220": { + "days": 9999, + "source": "rule", + "name": "Bicarbonato", + "location": "dispensa", + "ts": 1778419205 + }, + "a6d16a09fd9a6bfbd0a915f05dd71780": { + "days": 7, + "source": "ai", + "name": "Salsa Pronta Ciliegini", + "location": "frigo", + "ts": 1778419205 + }, + "4f8f1bb04a00e5fc62d7a9cfb21e1796": { + "days": 365, + "source": "rule", + "name": "Riso Chicchi Ricchi Gran Risparmio", + "location": "dispensa", + "ts": 1778419206 + }, + "e116e4c11084a463f9aaac02e1749fe7": { + "days": 90, + "source": "rule", + "name": "Salsa di soia", + "location": "dispensa", + "ts": 1778419207 + }, + "b1ad9afd4139b3f225b79af4dae256ce": { + "days": 60, + "source": "rule", + "name": "Tè Al limone", + "location": "dispensa", + "ts": 1778419504 + }, + "7ff2b7d326dcba52a664cebbf12f78a2": { + "days": 3, + "source": "ai", + "name": "Piselli fini 1\/2 vapore", + "location": "frigo", + "ts": 1778419505 + }, + "71062dc7ffd82b3ee4f40bad076a7c91": { + "days": 60, + "source": "rule", + "name": "Cioccolato bianco", + "location": "frigo", + "ts": 1778419506 + }, + "38a0eaea422dfe970eba125494e75981": { + "days": 180, + "source": "rule", + "name": "Zucca a pezzi", + "location": "freezer", + "ts": 1778419506 + }, + "cde21270e1cd50c431742e49117b225d": { + "days": 7, + "source": "rule", + "name": "Pancetta Dolce", + "location": "frigo", + "ts": 1778419507 + } } \ No newline at end of file diff --git a/data/shopping_total_cache.json b/data/shopping_total_cache.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/data/shopping_total_cache.json @@ -0,0 +1 @@ +{} diff --git a/index.html b/index.html index 1902fae..b5d8f95 100644 --- a/index.html +++ b/index.html @@ -1462,6 +1462,6 @@ - + diff --git a/manifest.json b/manifest.json index ce227bb..da1b585 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "name": "EverShelf", "short_name": "EverShelf", "description": "Gestione completa della dispensa di casa con scansione barcode", - "version": "1.7.7", + "version": "1.7.8", "start_url": "/evershelf/", "display": "standalone", "background_color": "#f0f4e8", diff --git a/translations/de.json b/translations/de.json index 8deca3f..2c5bc3e 100644 --- a/translations/de.json +++ b/translations/de.json @@ -471,7 +471,9 @@ "suggestion_snack_text": "Was kann ich als schnellen Snack machen?", "suggestion_juice_text": "Mach mir einen Saft oder Smoothie mit dem was ich habe", "suggestion_light_text": "Ich habe Hunger, möchte aber etwas Leichtes", - "suggestion_expiry_text": "Was läuft bald ab und wie kann ich es verwenden?" + "suggestion_expiry_text": "Was läuft bald ab und wie kann ich es verwenden?", + "use_ingredients_btn": "Zutaten verwenden", + "recipe_ingredients_from_pantry": "Im Vorratsschrank gefundene Zutaten" }, "cooking": { "close": "Schließen", diff --git a/translations/en.json b/translations/en.json index 582126c..8d3ba40 100644 --- a/translations/en.json +++ b/translations/en.json @@ -471,7 +471,9 @@ "suggestion_snack_text": "What can I make for a quick snack?", "suggestion_juice_text": "Make me a juice or smoothie with what I have", "suggestion_light_text": "I'm hungry but want something light", - "suggestion_expiry_text": "What's about to expire and how can I use it?" + "suggestion_expiry_text": "What's about to expire and how can I use it?", + "use_ingredients_btn": "Use ingredients", + "recipe_ingredients_from_pantry": "Ingredients found in pantry" }, "cooking": { "close": "Close", diff --git a/translations/it.json b/translations/it.json index b6344d2..bfa4bbf 100644 --- a/translations/it.json +++ b/translations/it.json @@ -471,7 +471,9 @@ "suggestion_snack_text": "Cosa posso preparare per uno spuntino veloce?", "suggestion_juice_text": "Fammi un succo o frullato con quello che ho", "suggestion_light_text": "Ho fame ma voglio qualcosa di leggero", - "suggestion_expiry_text": "Cosa sta per scadere e come posso usarlo?" + "suggestion_expiry_text": "Cosa sta per scadere e come posso usarlo?", + "use_ingredients_btn": "Usa ingredienti", + "recipe_ingredients_from_pantry": "Ingredienti trovati in dispensa" }, "cooking": { "close": "Chiudi",