Screensaver: orologio più grande, frasi dinamiche fade-in/out, font fact ingrandito
This commit is contained in:
@@ -2620,6 +2620,38 @@ body {
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Meal type selector */
|
||||
.recipe-meal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.recipe-meal-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 10px 6px;
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
border: 2px solid transparent;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.recipe-meal-chip:has(input:checked) {
|
||||
background: rgba(45, 80, 22, 0.1);
|
||||
border-color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.recipe-meal-chip input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ===== LARGER PRODUCT PREVIEW (Action page) ===== */
|
||||
.product-preview-large {
|
||||
flex-direction: column;
|
||||
@@ -3426,3 +3458,60 @@ body {
|
||||
color: var(--text-muted);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ===== SCREENSAVER ===== */
|
||||
.screensaver-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #000;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.8s ease;
|
||||
}
|
||||
.screensaver-overlay.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
.screensaver-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
padding: 20px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
.screensaver-clock {
|
||||
color: rgba(255,255,255,0.85);
|
||||
font-size: 7rem;
|
||||
font-weight: 200;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 4px;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
user-select: none;
|
||||
}
|
||||
.screensaver-clock .screensaver-date {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 300;
|
||||
opacity: 0.45;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.screensaver-fact {
|
||||
color: rgba(255,255,255,0.55);
|
||||
font-size: 1.6rem;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
max-width: 500px;
|
||||
min-height: 2.5em;
|
||||
opacity: 0;
|
||||
transition: opacity 1.5s ease;
|
||||
user-select: none;
|
||||
}
|
||||
.screensaver-fact.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
+481
-10
@@ -4085,18 +4085,31 @@ async function loadLog(more = false) {
|
||||
}
|
||||
|
||||
// ===== RECIPE GENERATION =====
|
||||
const MEAL_TYPES = [
|
||||
{ id: 'colazione', icon: '☀️', label: 'Colazione', from: 6, to: 9 },
|
||||
{ id: 'break', icon: '🥐', label: 'Break Mattutino', from: 9, to: 11 },
|
||||
{ id: 'pranzo', icon: '🍽️', label: 'Pranzo', from: 11, to: 14 },
|
||||
{ id: 'merenda', icon: '🍪', label: 'Merenda', from: 14, to: 17 },
|
||||
{ id: 'cena', icon: '🌙', label: 'Cena', from: 17, to: 22 },
|
||||
{ id: 'spuntino', icon: '🌜', label: 'Spuntino Serale', from: 22, to: 6 },
|
||||
];
|
||||
|
||||
function getMealType() {
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 5 && hour < 11) return 'colazione';
|
||||
if (hour >= 11 && hour < 16) return 'pranzo';
|
||||
for (const m of MEAL_TYPES) {
|
||||
if (m.from < m.to) { if (hour >= m.from && hour < m.to) return m.id; }
|
||||
else { if (hour >= m.from || hour < m.to) return m.id; }
|
||||
}
|
||||
return 'cena';
|
||||
}
|
||||
|
||||
const MEAL_LABELS = {
|
||||
'colazione': '☀️ Colazione',
|
||||
'pranzo': '🍽️ Pranzo',
|
||||
'cena': '🌙 Cena'
|
||||
};
|
||||
const MEAL_LABELS = {};
|
||||
MEAL_TYPES.forEach(m => { MEAL_LABELS[m.id] = `${m.icon} ${m.label}`; });
|
||||
|
||||
function getSelectedMealType() {
|
||||
const checked = document.querySelector('input[name="recipe-meal"]:checked');
|
||||
return checked ? checked.value : getMealType();
|
||||
}
|
||||
|
||||
// ===== RECIPE ARCHIVE (DB-backed) =====
|
||||
let _recipeArchiveCache = null;
|
||||
@@ -4203,9 +4216,18 @@ let _cachedRecipe = null;
|
||||
function openRecipeDialog() {
|
||||
const meal = getMealType();
|
||||
const settings = getSettings();
|
||||
document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta';
|
||||
document.getElementById('recipe-overlay').style.display = 'flex';
|
||||
|
||||
// Build meal selector radios
|
||||
const mealGrid = document.getElementById('recipe-meal-grid');
|
||||
if (mealGrid) {
|
||||
mealGrid.innerHTML = MEAL_TYPES.map(m => {
|
||||
const checked = m.id === meal ? ' checked' : '';
|
||||
return `<label class="recipe-meal-chip"><input type="radio" name="recipe-meal" value="${m.id}"${checked}> ${m.icon} ${m.label}</label>`;
|
||||
}).join('');
|
||||
}
|
||||
updateRecipeMealTitle();
|
||||
|
||||
// Check for cached recipe matching current meal type
|
||||
if (_cachedRecipe && _cachedRecipe.meal === meal && _cachedRecipe.recipe) {
|
||||
document.getElementById('recipe-ask').style.display = 'none';
|
||||
@@ -4367,18 +4389,31 @@ function renderRecipe(r) {
|
||||
document.getElementById('recipe-content').innerHTML = html;
|
||||
}
|
||||
|
||||
function updateRecipeMealTitle() {
|
||||
const meal = getSelectedMealType();
|
||||
document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta';
|
||||
}
|
||||
|
||||
function regenerateRecipe() {
|
||||
_cachedRecipe = null;
|
||||
document.getElementById('recipe-result').style.display = 'none';
|
||||
document.getElementById('recipe-loading').style.display = 'none';
|
||||
const meal = getMealType();
|
||||
document.getElementById('recipe-meal-title').textContent = MEAL_LABELS[meal] || '🍳 Ricetta';
|
||||
// Rebuild meal selector with auto-detected default
|
||||
const mealGrid = document.getElementById('recipe-meal-grid');
|
||||
if (mealGrid) {
|
||||
mealGrid.innerHTML = MEAL_TYPES.map(m => {
|
||||
const checked = m.id === meal ? ' checked' : '';
|
||||
return `<label class="recipe-meal-chip"><input type="radio" name="recipe-meal" value="${m.id}"${checked}> ${m.icon} ${m.label}</label>`;
|
||||
}).join('');
|
||||
}
|
||||
updateRecipeMealTitle();
|
||||
document.getElementById('recipe-persons').value = 1;
|
||||
document.getElementById('recipe-ask').style.display = '';
|
||||
}
|
||||
|
||||
async function generateRecipe() {
|
||||
const meal = getMealType();
|
||||
const meal = getSelectedMealType();
|
||||
const persons = parseInt(document.getElementById('recipe-persons').value) || 1;
|
||||
const settings = getSettings();
|
||||
|
||||
@@ -4614,10 +4649,446 @@ function saveChatHistory() {
|
||||
api('chat_save', {}, 'POST', { messages: newMsgs }).catch(() => {});
|
||||
}
|
||||
|
||||
// ===== SCREENSAVER & INACTIVITY AUTO-REFRESH =====
|
||||
let _inactivityTimer = null;
|
||||
let _screensaverActive = false;
|
||||
let _screensaverClockInterval = null;
|
||||
let _screensaverFactInterval = null;
|
||||
let _screensaverData = null; // cached data for fact generation
|
||||
const SCREENSAVER_FACT_DURATION = 5 * 60 * 1000; // 5 minutes per fact
|
||||
const INACTIVITY_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
function resetInactivityTimer() {
|
||||
if (_screensaverActive) return; // don't reset while screensaver is showing
|
||||
clearTimeout(_inactivityTimer);
|
||||
_inactivityTimer = setTimeout(activateScreensaver, INACTIVITY_TIMEOUT);
|
||||
}
|
||||
|
||||
function activateScreensaver() {
|
||||
if (_screensaverActive) return;
|
||||
_screensaverActive = true;
|
||||
const overlay = document.getElementById('screensaver');
|
||||
overlay.style.display = 'flex';
|
||||
// Fade in
|
||||
requestAnimationFrame(() => overlay.classList.add('visible'));
|
||||
updateScreensaverClock();
|
||||
_screensaverClockInterval = setInterval(updateScreensaverClock, 1000);
|
||||
// Load data and start facts
|
||||
loadScreensaverData().then(() => {
|
||||
showNextScreensaverFact();
|
||||
_screensaverFactInterval = setInterval(showNextScreensaverFact, SCREENSAVER_FACT_DURATION);
|
||||
});
|
||||
}
|
||||
|
||||
function updateScreensaverClock() {
|
||||
const now = new Date();
|
||||
const time = now.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
|
||||
const date = now.toLocaleDateString('it-IT', { weekday: 'long', day: 'numeric', month: 'long' });
|
||||
const el = document.getElementById('screensaver-clock');
|
||||
if (el) el.innerHTML = `${time}<div class="screensaver-date">${date}</div>`;
|
||||
}
|
||||
|
||||
function dismissScreensaver() {
|
||||
if (!_screensaverActive) return;
|
||||
clearInterval(_screensaverClockInterval);
|
||||
clearInterval(_screensaverFactInterval);
|
||||
const overlay = document.getElementById('screensaver');
|
||||
overlay.classList.remove('visible');
|
||||
setTimeout(() => {
|
||||
overlay.style.display = 'none';
|
||||
_screensaverActive = false;
|
||||
_screensaverData = null;
|
||||
// Reload all data for the current page
|
||||
refreshCurrentPage();
|
||||
resetInactivityTimer();
|
||||
}, 400);
|
||||
}
|
||||
|
||||
// Load all data needed for screensaver facts
|
||||
async function loadScreensaverData() {
|
||||
try {
|
||||
const [statsRes, invRes, bringRes] = await Promise.all([
|
||||
api('stats'),
|
||||
api('inventory_list'),
|
||||
api('bring_list').catch(() => null)
|
||||
]);
|
||||
_screensaverData = {
|
||||
stats: statsRes,
|
||||
inventory: invRes.inventory || [],
|
||||
shopping: bringRes && bringRes.success ? (bringRes.purchase || []) : []
|
||||
};
|
||||
} catch (e) {
|
||||
_screensaverData = { stats: {}, inventory: [], shopping: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// Show next random fact with fade in/out
|
||||
function showNextScreensaverFact() {
|
||||
const el = document.getElementById('screensaver-fact');
|
||||
if (!el) return;
|
||||
el.classList.remove('visible');
|
||||
setTimeout(() => {
|
||||
el.textContent = generateScreensaverFact();
|
||||
el.classList.add('visible');
|
||||
}, 1600);
|
||||
}
|
||||
|
||||
// Generate a dynamic fact from available data
|
||||
function generateScreensaverFact() {
|
||||
const d = _screensaverData || { stats: {}, inventory: [], shopping: [] };
|
||||
const inv = d.inventory;
|
||||
const stats = d.stats;
|
||||
const shop = d.shopping;
|
||||
const now = new Date();
|
||||
const hour = now.getHours();
|
||||
|
||||
// Pre-compute useful data
|
||||
const expired = stats.expired || [];
|
||||
const expiringSoon = stats.expiring_soon || [];
|
||||
const totalProducts = stats.total_products || inv.length;
|
||||
const totalItems = stats.total_items || 0;
|
||||
|
||||
const byLocation = {};
|
||||
const byCategory = {};
|
||||
const withExpiry = [];
|
||||
const noExpiry = [];
|
||||
const expiringThisWeek = [];
|
||||
const expiringThisMonth = [];
|
||||
const inFreezer = [];
|
||||
const inFrigo = [];
|
||||
const inDispensa = [];
|
||||
|
||||
for (const item of inv) {
|
||||
// by location
|
||||
const loc = item.location || 'altro';
|
||||
if (!byLocation[loc]) byLocation[loc] = [];
|
||||
byLocation[loc].push(item);
|
||||
if (loc === 'freezer') inFreezer.push(item);
|
||||
else if (loc === 'frigo') inFrigo.push(item);
|
||||
else if (loc === 'dispensa') inDispensa.push(item);
|
||||
|
||||
// by category
|
||||
const cat = mapToLocalCategory(item.category, item.name);
|
||||
if (!byCategory[cat]) byCategory[cat] = [];
|
||||
byCategory[cat].push(item);
|
||||
|
||||
// expiry
|
||||
if (item.expiry_date) {
|
||||
withExpiry.push(item);
|
||||
const days = daysUntilExpiry(item.expiry_date);
|
||||
if (days >= 0 && days <= 7) expiringThisWeek.push(item);
|
||||
if (days >= 0 && days <= 30) expiringThisMonth.push(item);
|
||||
} else {
|
||||
noExpiry.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Greeting based on time
|
||||
const greeting = hour < 12 ? 'Buongiorno' : hour < 18 ? 'Buon pomeriggio' : 'Buonasera';
|
||||
|
||||
// Estimated shopping total
|
||||
let spesaTotal = 0;
|
||||
let spesaPriced = 0;
|
||||
for (const item of shop) {
|
||||
const pd = shoppingPrices[item.name.toLowerCase()];
|
||||
if (pd && pd.product) {
|
||||
const est = estimateItemPrice(pd.product, item.specification || pd.spec || '');
|
||||
spesaTotal += est ? est.estimated : pd.product.price;
|
||||
spesaPriced++;
|
||||
}
|
||||
}
|
||||
|
||||
// Random item picker
|
||||
const rItem = (arr) => arr.length ? arr[Math.floor(Math.random() * arr.length)] : null;
|
||||
|
||||
// All fact generators
|
||||
const facts = [];
|
||||
|
||||
// --- Expired items facts ---
|
||||
if (expired.length > 0) {
|
||||
facts.push(() => `Hai ${expired.length} ${expired.length === 1 ? 'prodotto scaduto' : 'prodotti scaduti'} in dispensa. Controlla!`);
|
||||
facts.push(() => {
|
||||
const names = expired.slice(0, 3).map(i => i.name);
|
||||
return `Prodotti scaduti: ${names.join(', ')}${expired.length > 3 ? ` e altri ${expired.length - 3}` : ''}`;
|
||||
});
|
||||
const freezerExpired = expired.filter(i => i.location === 'freezer');
|
||||
if (freezerExpired.length > 0) {
|
||||
facts.push(() => {
|
||||
const item = rItem(freezerExpired);
|
||||
const safety = getExpiredSafety(item, Math.abs(daysUntilExpiry(item.expiry_date)));
|
||||
if (safety.level === 'ok' || safety.level === 'warning') {
|
||||
return `${item.name} è scaduto, ma essendo in freezer potrebbe essere ancora buono! Controlla.`;
|
||||
}
|
||||
return `${item.name} in freezer è scaduto da troppo tempo. Meglio buttarlo.`;
|
||||
});
|
||||
}
|
||||
const frigoExpired = expired.filter(i => i.location === 'frigo');
|
||||
if (frigoExpired.length > 0) {
|
||||
facts.push(() => `Hai ${frigoExpired.length} ${frigoExpired.length === 1 ? 'prodotto scaduto' : 'prodotti scaduti'} in frigo!`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Expiring soon facts ---
|
||||
if (expiringSoon.length > 0) {
|
||||
facts.push(() => {
|
||||
const item = expiringSoon[0];
|
||||
const days = daysUntilExpiry(item.expiry_date);
|
||||
if (days === 0) return `${item.name} scade oggi! Usalo subito.`;
|
||||
if (days === 1) return `${item.name} scade domani. Pensaci!`;
|
||||
return `${item.name} scade tra ${days} giorni.`;
|
||||
});
|
||||
if (expiringSoon.length > 1) {
|
||||
facts.push(() => `Hai ${expiringSoon.length} prodotti in scadenza ravvicinata.`);
|
||||
}
|
||||
}
|
||||
if (expiringThisWeek.length > 0) {
|
||||
facts.push(() => `Questa settimana scadono ${expiringThisWeek.length} prodotti. Pianifica i pasti di conseguenza!`);
|
||||
facts.push(() => {
|
||||
const item = rItem(expiringThisWeek);
|
||||
const days = daysUntilExpiry(item.expiry_date);
|
||||
const locLabel = LOCATIONS[item.location]?.label || item.location;
|
||||
return `${item.name} (${locLabel}) scade tra ${days} ${days === 1 ? 'giorno' : 'giorni'}.`;
|
||||
});
|
||||
}
|
||||
if (expiringThisMonth.length > 0) {
|
||||
facts.push(() => `In questo mese scadranno ${expiringThisMonth.length} prodotti.`);
|
||||
}
|
||||
|
||||
// --- Shopping list facts ---
|
||||
if (shop.length > 0) {
|
||||
facts.push(() => `Hai ${shop.length} ${shop.length === 1 ? 'prodotto' : 'prodotti'} nella lista della spesa.`);
|
||||
facts.push(() => {
|
||||
const names = shop.slice(0, 4).map(i => i.name);
|
||||
return `Nella spesa: ${names.join(', ')}${shop.length > 4 ? '...' : ''}`;
|
||||
});
|
||||
if (spesaTotal > 0) {
|
||||
facts.push(() => `Il totale previsto per la spesa è circa €${spesaTotal.toFixed(2)}.`);
|
||||
if (spesaPriced < shop.length) {
|
||||
facts.push(() => `Spesa stimata: €${spesaTotal.toFixed(2)} (${spesaPriced} di ${shop.length} prodotti con prezzo).`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shop.length === 0) {
|
||||
facts.push(() => `La lista della spesa è vuota. Tutto a posto!`);
|
||||
}
|
||||
|
||||
// --- Location-based facts ---
|
||||
if (inFrigo.length > 0) {
|
||||
facts.push(() => `Hai ${inFrigo.length} prodotti in frigo.`);
|
||||
facts.push(() => {
|
||||
const item = rItem(inFrigo);
|
||||
return `In frigo c'è: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}.`;
|
||||
});
|
||||
}
|
||||
if (inFreezer.length > 0) {
|
||||
facts.push(() => `Hai ${inFreezer.length} prodotti nel freezer.`);
|
||||
facts.push(() => {
|
||||
const item = rItem(inFreezer);
|
||||
return `Nel freezer c'è: ${item.name}. Non dimenticartelo!`;
|
||||
});
|
||||
}
|
||||
if (inDispensa.length > 0) {
|
||||
facts.push(() => `In dispensa ci sono ${inDispensa.length} prodotti.`);
|
||||
}
|
||||
|
||||
// --- Category-based facts ---
|
||||
const catEntries = Object.entries(byCategory);
|
||||
if (catEntries.length > 0) {
|
||||
facts.push(() => {
|
||||
const sorted = catEntries.sort((a, b) => b[1].length - a[1].length);
|
||||
const top = sorted[0];
|
||||
const catLabel = top[0];
|
||||
const icon = CATEGORY_ICONS[catLabel] || '📦';
|
||||
return `La categoria più presente è ${icon} ${catLabel} con ${top[1].length} prodotti.`;
|
||||
});
|
||||
if (byCategory['carne'] && byCategory['carne'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['carne'].length} prodotti di carne. 🥩`);
|
||||
}
|
||||
if (byCategory['latticini'] && byCategory['latticini'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['latticini'].length} latticini in casa. 🥛`);
|
||||
}
|
||||
if (byCategory['verdura'] && byCategory['verdura'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['verdura'].length} tipi di verdura. Ottimo per la salute! 🥬`);
|
||||
}
|
||||
if (byCategory['frutta'] && byCategory['frutta'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['frutta'].length} tipi di frutta. 🍎`);
|
||||
}
|
||||
if (byCategory['bevande'] && byCategory['bevande'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['bevande'].length} bevande disponibili. 🥤`);
|
||||
}
|
||||
if (byCategory['surgelati'] && byCategory['surgelati'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['surgelati'].length} surgelati nel freezer. ❄️`);
|
||||
}
|
||||
if (byCategory['pasta'] && byCategory['pasta'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['pasta'].length} tipi di pasta. 🍝 Che ne dici di una carbonara?`);
|
||||
}
|
||||
if (byCategory['conserve'] && byCategory['conserve'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['conserve'].length} conserve in dispensa. 🥫`);
|
||||
}
|
||||
if (byCategory['snack'] && byCategory['snack'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['snack'].length} snack. Resisti alla tentazione! 🍪`);
|
||||
}
|
||||
if (byCategory['condimenti'] && byCategory['condimenti'].length > 0) {
|
||||
facts.push(() => `Hai ${byCategory['condimenti'].length} condimenti a disposizione. 🧂`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- General inventory facts ---
|
||||
if (inv.length > 0) {
|
||||
facts.push(() => `Hai ${totalProducts} prodotti diversi in casa per un totale di ${Math.round(totalItems)} pezzi.`);
|
||||
facts.push(() => {
|
||||
const item = rItem(inv);
|
||||
return `Lo sapevi? Hai ${item.name} in ${LOCATIONS[item.location]?.label || item.location}.`;
|
||||
});
|
||||
facts.push(() => {
|
||||
const item = rItem(inv);
|
||||
const qty = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
|
||||
return `${item.name}: ne hai ${qty}.`;
|
||||
});
|
||||
}
|
||||
if (noExpiry.length > 0) {
|
||||
facts.push(() => `${noExpiry.length} prodotti non hanno una data di scadenza impostata.`);
|
||||
}
|
||||
if (withExpiry.length > 0) {
|
||||
// Find the one expiring furthest away
|
||||
const furthest = withExpiry.reduce((best, item) => {
|
||||
const d = daysUntilExpiry(item.expiry_date);
|
||||
return d > (best.d || 0) ? { item, d } : best;
|
||||
}, { d: 0 });
|
||||
if (furthest.item && furthest.d > 30) {
|
||||
facts.push(() => `Il prodotto con scadenza più lontana è ${furthest.item.name}: ${Math.round(furthest.d / 30)} mesi.`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Quantity-based facts ---
|
||||
const highQtyItems = inv.filter(i => parseFloat(i.quantity) >= 5);
|
||||
if (highQtyItems.length > 0) {
|
||||
facts.push(() => {
|
||||
const item = rItem(highQtyItems);
|
||||
const qty = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
|
||||
return `Hai una bella scorta di ${item.name}: ${qty}!`;
|
||||
});
|
||||
}
|
||||
const lowQtyItems = inv.filter(i => parseFloat(i.quantity) <= 1 && parseFloat(i.quantity) > 0);
|
||||
if (lowQtyItems.length > 0) {
|
||||
facts.push(() => {
|
||||
const item = rItem(lowQtyItems);
|
||||
return `${item.name} sta per finire. Aggiungilo alla spesa?`;
|
||||
});
|
||||
facts.push(() => `Ci sono ${lowQtyItems.length} prodotti quasi finiti.`);
|
||||
}
|
||||
|
||||
// --- Time-of-day greetings & suggestions ---
|
||||
facts.push(() => `${greeting}! Se vuoi che ti preparo una ricetta, tocca qui.`);
|
||||
facts.push(() => `${greeting}! La tua dispensa è sotto controllo. 😊`);
|
||||
if (hour >= 6 && hour < 10) {
|
||||
facts.push(() => `Buongiorno! Pronto per la colazione? ☕`);
|
||||
if (byCategory['pane']) facts.push(() => `Buongiorno! Hai del pane per la colazione. 🍞`);
|
||||
if (byCategory['latticini']) facts.push(() => `C'è del latte in frigo per il cappuccino? ☕🥛`);
|
||||
}
|
||||
if (hour >= 11 && hour < 14) {
|
||||
facts.push(() => `È quasi ora di pranzo! Cosa cuciniamo? 🍽️`);
|
||||
if (byCategory['pasta']) facts.push(() => `Ora di pranzo… Un bel piatto di pasta? 🍝`);
|
||||
}
|
||||
if (hour >= 17 && hour < 21) {
|
||||
facts.push(() => `Buona sera! Hai pensato alla cena? 🍽️`);
|
||||
if (byCategory['carne']) facts.push(() => `Per cena potresti usare la carne che hai. 🥩`);
|
||||
if (byCategory['pesce']) facts.push(() => `Che ne dici di pesce per cena? 🐟`);
|
||||
}
|
||||
if (hour >= 21 || hour < 6) {
|
||||
facts.push(() => `Buonanotte! Domani controlla le scadenze. 🌙`);
|
||||
}
|
||||
|
||||
// --- Weekly stats ---
|
||||
const recentIn = stats.recent_in || 0;
|
||||
const recentOut = stats.recent_out || 0;
|
||||
if (recentIn > 0) {
|
||||
facts.push(() => `Questa settimana hai aggiunto ${recentIn} prodotti.`);
|
||||
}
|
||||
if (recentOut > 0) {
|
||||
facts.push(() => `Questa settimana hai consumato ${recentOut} prodotti.`);
|
||||
}
|
||||
if (recentIn > 0 && recentOut > 0) {
|
||||
facts.push(() => `Bilancio settimanale: +${recentIn} entrati, -${recentOut} usciti.`);
|
||||
}
|
||||
|
||||
// --- Tips & curiosità (statici ma ruotano) ---
|
||||
facts.push(() => `💡 Lo sapevi? I prodotti in freezer durano molto più a lungo della data di scadenza.`);
|
||||
facts.push(() => `💡 Il pane congelato mantiene la fragranza per settimane.`);
|
||||
facts.push(() => `💡 Le uova si conservano fino a 3-4 settimane dopo la data preferita.`);
|
||||
facts.push(() => `💡 Lo yogurt chiuso in frigo dura spesso 1-2 settimane oltre la scadenza.`);
|
||||
facts.push(() => `💡 Per evitare sprechi, usa prima i prodotti con scadenza più vicina.`);
|
||||
facts.push(() => `💡 La carne in freezer può durare fino a 6 mesi senza problemi.`);
|
||||
facts.push(() => `💡 Le verdure fresche durano di più se conservate nel cassetto del frigo.`);
|
||||
facts.push(() => `💡 Controlla regolarmente la dispensa per evitare doppioni nella spesa.`);
|
||||
facts.push(() => `💡 I latticini vanno conservati nella parte più fredda del frigo.`);
|
||||
facts.push(() => `💡 Non ricongelare mai un alimento già scongelato. Cucinalo subito!`);
|
||||
facts.push(() => `💡 Un frigo ordinato ti fa risparmiare tempo e denaro.`);
|
||||
facts.push(() => `💡 Le conserve aperte vanno in frigo e consumate in pochi giorni.`);
|
||||
|
||||
// --- Brand-based facts ---
|
||||
const brands = inv.filter(i => i.brand).map(i => i.brand);
|
||||
if (brands.length > 0) {
|
||||
const brandCount = {};
|
||||
brands.forEach(b => { brandCount[b] = (brandCount[b] || 0) + 1; });
|
||||
const topBrand = Object.entries(brandCount).sort((a, b) => b[1] - a[1])[0];
|
||||
facts.push(() => `Il marca più presente nella tua dispensa è ${topBrand[0]} con ${topBrand[1]} prodotti.`);
|
||||
}
|
||||
|
||||
// --- Specific food combo facts ---
|
||||
if (byCategory['pasta'] && byCategory['condimenti']) {
|
||||
facts.push(() => `Hai pasta e condimenti: sei pronto per un primo piatto! 🍝`);
|
||||
}
|
||||
if (byCategory['pane'] && byCategory['carne']) {
|
||||
facts.push(() => `Pane e carne: un panino veloce è sempre una buona idea! 🥪`);
|
||||
}
|
||||
if (byCategory['verdura'] && byCategory['carne']) {
|
||||
facts.push(() => `Verdura e carne: hai tutto per un piatto equilibrato! 🥗🥩`);
|
||||
}
|
||||
|
||||
// --- Empty states ---
|
||||
if (inv.length === 0) {
|
||||
facts.push(() => `La dispensa è vuota! Fai una bella spesa. 🛒`);
|
||||
facts.push(() => `Nessun prodotto registrato. Scansiona qualcosa per iniziare!`);
|
||||
}
|
||||
|
||||
// --- Location distribution ---
|
||||
const locCount = Object.keys(byLocation).length;
|
||||
if (locCount > 1) {
|
||||
facts.push(() => {
|
||||
const parts = Object.entries(byLocation).map(([loc, items]) =>
|
||||
`${LOCATIONS[loc]?.icon || '📦'} ${items.length}`
|
||||
);
|
||||
return `Distribuzione: ${parts.join(' · ')}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Pick a random fact
|
||||
if (facts.length === 0) {
|
||||
return `${greeting}! La tua Dispensa ti aspetta.`;
|
||||
}
|
||||
return facts[Math.floor(Math.random() * facts.length)]();
|
||||
}
|
||||
|
||||
function initInactivityWatcher() {
|
||||
const events = ['pointerdown', 'pointermove', 'keydown', 'scroll', 'touchstart'];
|
||||
events.forEach(evt => {
|
||||
document.addEventListener(evt, () => {
|
||||
if (_screensaverActive) {
|
||||
dismissScreensaver();
|
||||
} else {
|
||||
resetInactivityTimer();
|
||||
}
|
||||
}, { passive: true });
|
||||
});
|
||||
resetInactivityTimer();
|
||||
}
|
||||
|
||||
// ===== INITIALIZATION =====
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
syncSettingsFromDB();
|
||||
showPage('dashboard');
|
||||
initInactivityWatcher();
|
||||
});
|
||||
|
||||
// ===== DUPLICLICK (SPESA ONLINE) =====
|
||||
|
||||
Binary file not shown.
+14
-2
@@ -9,7 +9,7 @@
|
||||
<title>Dispensa Manager</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260312t">
|
||||
<link rel="stylesheet" href="assets/css/style.css?v=20260313b">
|
||||
<!-- QuaggaJS for barcode scanning -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||
</head>
|
||||
@@ -802,6 +802,10 @@
|
||||
<div id="recipe-ask" class="recipe-ask">
|
||||
<h3 id="recipe-meal-title">🍳 Ricetta</h3>
|
||||
<p class="recipe-desc">Genero una ricetta sana con gli ingredienti in dispensa, dando priorità a quelli in scadenza.</p>
|
||||
<div class="form-group" style="text-align:left">
|
||||
<label>🕐 Per quale pasto?</label>
|
||||
<div class="recipe-meal-grid" id="recipe-meal-grid" onchange="updateRecipeMealTitle()"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>👥 Quante persone?</label>
|
||||
<div class="qty-control">
|
||||
@@ -858,6 +862,14 @@
|
||||
<div class="modal-content" id="modal-content" onclick="event.stopPropagation()"></div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js?v=20260312z"></script>
|
||||
<!-- Screensaver Overlay -->
|
||||
<div id="screensaver" class="screensaver-overlay" style="display:none">
|
||||
<div class="screensaver-content">
|
||||
<div class="screensaver-clock" id="screensaver-clock"></div>
|
||||
<div class="screensaver-fact" id="screensaver-fact"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js?v=20260313b"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user