1// recettes / tutos : multiplicateur de portions, cases à cocher, mode cuisine (recette v2) 2document.addEventListener('DOMContentLoaded', function() { 3 var countInput = document.querySelector('.count-ingredient'); 4 5 /* ---------------- multiplicateur de portions ---------------- */ 6 if (countInput) { 7 var basePersonnes = parseInt(countInput.value, 10) || 1; 8 9 // Affichage "cuisine" : entiers tels quels, fractions usuelles (¼ â ½ â ¾), sinon 1 décimale. 10 // Le multiplicateur relit toujours data-base-quantite (jamais le texte affiché), donc l'affichage 11 // en fraction ne casse pas les calculs suivants. 12 var FRACTIONS = [[0.25, '¼'], [0.33, 'â '], [0.5, '½'], [0.66, 'â '], [0.75, '¾']]; 13 function formatQty(q) { 14 if (Math.abs(q - Math.round(q)) < 0.02) return String(Math.round(q)); 15 var entier = Math.floor(q), reste = q - entier; 16 for (var i = 0; i < FRACTIONS.length; i++) { 17 if (Math.abs(reste - FRACTIONS[i][0]) < 0.04) return (entier ? entier + ' ' : '') + FRACTIONS[i][1]; 18 } 19 return (Math.round(q * 10) / 10).toString().replace('.', ','); 20 } 21 function parseQty(text) { 22 // accepte "0.5", "0,5", "1/2", "1 1/2", "½" 23 var t = (text || '').trim().replace(',', '.'); 24 var uni = {'¼': 0.25, 'â ': 0.333, '½': 0.5, 'â ': 0.666, '¾': 0.75}; 25 var m = t.match(/^(\d+)?\s*([¼â ½â ¾])$/); 26 if (m) return (parseInt(m[1] || '0', 10)) + uni[m[2]]; 27 m = t.match(/^(\d+)\s+(\d+)\/(\d+)$/); 28 if (m) return parseInt(m[1], 10) + parseInt(m[2], 10) / parseInt(m[3], 10); 29 m = t.match(/^(\d+)\/(\d+)$/); 30 if (m) return parseInt(m[1], 10) / parseInt(m[2], 10); 31 var f = parseFloat(t); 32 return isNaN(f) ? null : f; 33 } 34 function updateQuantities(factor) { 35 document.querySelectorAll('.ingredient .chiffre-ingredient').forEach(function(element) { 36 var base = element.dataset.baseQuantite; 37 if (base === undefined) { 38 var parsed = parseQty(element.textContent); 39 element.dataset.baseQuantite = parsed === null ? '' : String(parsed); 40 base = element.dataset.baseQuantite; 41 } 42 if (base === '') return; 43 var q = parseFloat(base) * factor / basePersonnes; 44 element.textContent = formatQty(q); 45 // unité au pluriel/singulier selon la quantité (recette v2 : data-sing / data-plur posés par le rendu) 46 var unite = element.parentNode ? element.parentNode.querySelector('.pai-unite[data-plur]') : null; 47 if (unite) unite.textContent = q > 1 ? unite.dataset.plur : unite.dataset.sing; 48 }); 49 } 50 function setCount(n) { 51 n = Math.max(1, parseInt(n, 10) || 1); 52 countInput.value = n; 53 updateQuantities(n); 54 } 55 document.querySelectorAll('.fa-plus,.pai-plus').forEach(function(b) { b.addEventListener('click', function() { setCount(parseInt(countInput.value, 10) + 1); }); }); 56 document.querySelectorAll('.fa-minus,.pai-moins').forEach(function(b) { b.addEventListener('click', function() { setCount(parseInt(countInput.value, 10) - 1); }); }); 57 countInput.addEventListener('change', function() { setCount(this.value); }); 58 updateQuantities(basePersonnes); // affichage "cuisine" dès le chargement (0.5 â ½) 59 } 60 61 document.querySelectorAll('.ingredient input[type=checkbox]').forEach(function(checkbox) { 62 checkbox.addEventListener('click', function() { this.nextElementSibling.classList.toggle('barre'); }); 63 }); 64 65 /* ---------------- mode cuisine (recette v2 : étapes ancrées #etape-n) ---------------- */ 66 var prepH2 = document.getElementById('preparation'); 67 var stepHeadings = Array.prototype.slice.call(document.querySelectorAll('h3[id^="etape-"]')); 68 if (!prepH2 || !stepHeadings.length) return; 69 70 var lang = (document.documentElement.lang || 'en').substring(0, 2).toLowerCase(); 71 var L = { 72 fr: { on: 'Mode cuisine', off: 'Quitter le mode cuisine', activate: 'Activer', desc: 'Ãcran qui reste allumé, étapes à cocher, minuteur à chaque étape : pensé pour cuisiner avec le téléphone à côté de la planche.', timer: 'Minuteur', start: '
72Lancer', stop: 'Stop', done: 'Terminé !', reset: 'Réinitialiser' }, 73 en: { on: 'Cooking mode', off: 'Exit cooking mode', activate: 'Turn on', desc: 'Screen stays on, steps you can tick off, a timer for each step: made for cooking with your phone next to the board.', timer: 'Timer', start: 'Start', stop: 'Stop', done: 'Done!', reset: 'Reset' }, 74 it: { on: 'Modalità cucina', off: 'Esci dalla modalità cucina', activate: 'Attiva', desc: 'Schermo sempre acceso, passaggi da spuntare, timer per ogni passaggio: pensata per cucinare con il telefono accanto al tagliere.', timer: 'Timer', start: 'Avvia', stop: 'Stop', done: 'Fatto!', reset: 'Azzera' }, 75 de: { on: 'Kochmodus', off: 'Kochmodus beenden', activate: 'Einschalten', desc: 'Bildschirm bleibt an, Schritte zum Abhaken, Timer für jeden Schritt: zum Kochen mit dem Handy neben dem Brett.', timer: 'Timer', start: 'Start', stop: 'Stopp', done: 'Fertig!', reset: 'Zurücksetzen' }, 76 es: { on: 'Modo cocina', off: 'Salir del modo cocina', activate: 'Activar', desc: 'Pantalla siempre encendida, pasos para marcar, temporizador en cada paso: pensado para cocinar con el móvil junto a la tabla.', timer: 'Temporizador', start: 'Iniciar', stop: 'Parar', done: '¡Listo!', reset: 'Reiniciar' }, 77 pl: { on: 'Tryb gotowania', off: 'Wyjdź z trybu gotowania', activate: 'WÅÄ cz', desc: 'Ekran nie gaÅnie, kroki do odhaczania, minutnik przy każdym kroku: do gotowania z telefonem obok deski.', timer: 'Minutnik', start: 'Start', stop: 'Stop', done: 'Gotowe!', reset: 'Resetuj' } 78 }[lang] || null; 79 if (!L) L = { on: 'Cooking mode', off: 'Exit cooking mode', activate: 'Turn on', desc: 'Screen stays on, steps you can tick off, a timer for each step.', timer: 'Timer', start: 'Start', stop: 'Stop', done: 'Done!', reset: 'Reset' }; 80 81 // 1) regroupe chaque étape (h3 + ses frères jusqu'au prochain h2/h3) dans un div.pai-etape 82 var storeKey = 'pai-cook-' + location.pathname; 83 var doneSteps = []; 84 try { doneSteps = JSON.parse(localStorage.getItem(storeKey) || '[]'); } catch (e) { doneSteps = []; } 85
86 stepHeadings.forEach(function(h3) { 87 var wrap = document.createElement('div'); 88 wrap.className = 'pai-etape'; 89 wrap.dataset.step = h3.id; 90 h3.parentNode.insertBefore(wrap, h3); 91 var node = h3; 92 while (node && !(node.nodeType === 1 && /^H[23]$/.test(node.tagName) && node !== h3)) { 93 var next = node.nextSibling; 94 wrap.appendChild(node); 95 node = next; 96 } 97 // case à cocher "étape faite" 98 var cb = document.createElement('input'); 99 cb.type = 'checkbox'; cb.className = 'pai-etape-check'; cb.setAttribute('aria-label', h3.textContent); 100 cb.checked = doneSteps.indexOf(h3.id) !== -1; 101 if (cb.checked) wrap.classList.add('pai-etape-done'); 102 cb.addEventListener('change', function() { 103 wrap.classList.toggle('pai-etape-done', cb.checked); 104 var idx = doneSteps.indexOf(h3.id); 105 if (cb.checked && idx === -1) doneSteps.push(h3.id); 106 if (!cb.checked && idx !== -1) doneSteps.splice(idx, 1); 107 try { localStorage.setItem(storeKey, JSON.stringify(doneSteps)); } catch (e) {} 108 // étape courante = première non cochée 109 var current = document.querySelector('.pai-etape:not(.pai-etape-done)'); 110 document.querySelectorAll('.pai-etape').forEach(function(w) { w.classList.toggle('pai-etape-current', w === current); }); 111 }); 112 h3.insertBefore(cb, h3.firstChild); 113 114 // minuteur sur les étapes qui ont une durée 115 var duree = wrap.querySelector('.pai-etape-duree'); 116 if (duree) { 117 var minutes = parseInt((duree.textContent.match(/(\d+)/) || [0, 0])[1], 10); 118 if (minutes > 0) buildTimer(duree, minutes); 119 } 120 }); 121 var firstOpen = document.querySelector('.pai-etape:not(.pai-etape-done)'); 122 if (firstOpen) firstOpen.classList.add('pai-etape-current'); 123 124 // 2) encart "mode cuisine" sous le H2 Préparation : une carte couleur thème qui explique ce que ça fait + bouton 125 var box = document.createElement('div'); 126 box.className = 'pai-cards'; 127 box.innerHTML = '<div class="pai-card pai-card-primary pai-card-full pai-cookmode-box"><div class="pai-cookmode-text"><small>ð¨âð³ ' + L.on + '</small><p></p></div></div>'; 128 box.querySelector('p').textContent = L.desc; 129 var btn = document.createElement('button'); 130 btn.type = 'button'; btn.className = 'pai-cookmode-btn'; btn.textContent = L.activate; 131 box.firstChild.appendChild(btn); 132 prepH2.parentNode.insertBefore(box, prepH2.nextSibling); 133 134 var wakeLock = null; 135 function requestWakeLock() { 136 if (!('wakeLock' in navigator)) return; 137 navigator.wakeLock.request('screen').then(function(lock) { wakeLock = lock; }).catch(function() {}); 138 } 139 function releaseWakeLock() { if (wakeLock) { wakeLock.release().catch(function() {}); wakeLock = null; } } 140 document.addEventListener('visibilitychange', function() { 141 if (document.body.classList.contains('pai-cookmode') && document.visibilityState === 'visible') requestWakeLock(); 142 }); 143 btn.addEventListener('click', function() { 144 var on = document.body.classList.toggle('pai-cookmode'); 145 btn.textContent = on ? 'â ' + L.off : L.activate; 146 if (on) { requestWakeLock(); var cur = document.querySelector('.pai-etape-current'); if (cur) cur.scrollIntoView({ behavior: 'smooth', block: 'start' }); } 147 else releaseWakeLock(); 148 }); 149 150 // 3) minuteur : compte à rebours + bip (Web Audio, pas de fichier) + notification si autorisée 151 function beep() { 152 try { 153 var ctx = new (window.AudioContext || window.webkitAudioContext)(); 154 [0, 0.35, 0.7].forEach(function(t) { 155 var o = ctx.createOscillator(), g = ctx.createGain(); 156 o.frequency.value = 880; o.connect(g); g.connect(ctx.destination); 157 g.gain.setValueAtTime(0.0001, ctx.currentTime + t); 158 g.gain.exponentialRampToValueAtTime(0.3, ctx.currentTime + t + 0.02); 159 g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + t + 0.25); 160 o.start(ctx.currentTime + t); o.stop(ctx.currentTime + t + 0.3); 161 }); 162 } catch (e) {} 163 } 164 function buildTimer(anchorEl, minutes) { 165 var box = document.createElement('span'); 166 box.className = 'pai-timer'; 167 var display = document.createElement('span'); display.className = 'pai-timer-display'; display.textContent = pad(minutes) + ':00'; 168 var start = document.createElement('button'); start.type = 'button'; start.className = 'pai-timer-btn'; start.textContent = 'â¶ ' + L.start; 169 box.appendChild(display); box.appendChild(start); 170 anchorEl.parentNode.insertBefore(box, anchorEl.nextSibling); 171 var remaining = minutes * 60, handle = null; 172 function render() { display.textContent = pad(Math.floor(remaining / 60)) + ':' + pad(remaining % 60); } 173 function stop() { if (handle) clearInterval(handle); handle = null; start.textContent = remaining === minutes * 60 ? 'â¶ ' + L.start : '⺠' + L.reset; box.classList.remove('pai-timer-running'); } 174 start.addEventListener('click', function() { 175 if (handle) { stop(); return; } 176 if (remaining <= 0) { remaining = minutes * 60; render(); stop(); return; } 177 if (start.textContent.indexOf('âº') === 0) { remaining = minutes * 60; render(); start.textContent = 'â¶ ' + L.start; return; } 178 if ('Notification' in window && Notification.permission === 'default') { try { Notification.requestPermission(); } catch (e) {} } 179 box.classList.add('pai-timer-running'); start.textContent = 'â ' + L.stop; 180 handle = setInterval(function() { 181 remaining--; render(); 182 if (remaining <= 0) { 183 clearInterval(handle); handle = null; 184 box.classList.remove('pai-timer-running'); box.classList.add('pai-timer-done'); 185 display.textContent = L.done; start.textContent = '⺠' + L.reset; 186 beep(); 187 if ('Notification' in window && Notification.permission === 'granted') { try { new Notification(L.timer + ' â ' + L.done); } catch (e) {} } 188 setTimeout(function() { box.classList.remove('pai-timer-done'); }, 4000); 189 } 190 }, 1000); 191 }); 192 } 193 function pad(n) { return (n < 10 ? '0' : '') + n; } 194});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.