diff --git a/public/userarea/class/Kiosk.php b/public/userarea/class/Kiosk.php index ba8cb431..3dca6484 100644 --- a/public/userarea/class/Kiosk.php +++ b/public/userarea/class/Kiosk.php @@ -22,6 +22,7 @@ class Kiosk 'andamento' => 'Andamento 14 giorni', 'fermi' => 'Fermi della settimana', 'coda' => 'Lavoro in arrivo', + 'meteo' => 'Meteo e previsioni', 'sicurezza' => 'Giorni senza infortuni', 'obiettivi' => 'Obiettivi', 'comunicazioni' => 'Comunicazioni', @@ -51,7 +52,8 @@ class Kiosk const DEFAULTS = [ 'company' => 'Zibo', 'slide_seconds' => '15', - 'slides' => 'linee,oggi,andamento,fermi,coda,sicurezza,obiettivi,comunicazioni,benvenuti,turni,menu', + 'slide_durations' => '', + 'slides' => 'linee,oggi,andamento,fermi,coda,meteo,sicurezza,obiettivi,comunicazioni,benvenuti,turni,menu', 'scarto_target' => '3', 'last_incident' => '', 'record_days' => '', @@ -91,6 +93,16 @@ class Kiosk return $t; } + /** Secondi per tipo di slide (vuoto = durata predefinita). */ + public static function durations(array $s) + { + $out = []; + foreach ((array)json_decode((string)$s['slide_durations'], true) as $k => $v) { + if (isset(self::SLIDES[$k]) && (int)$v > 0) $out[$k] = max(4, min(300, (int)$v)); + } + return $out; + } + /* ===================== DATE ===================== */ private static function tz() @@ -366,6 +378,7 @@ class Kiosk 'settings' => [ 'slide_seconds' => max(6, min(120, (int)$s['slide_seconds'] ?: 15)), 'slides' => array_values(array_filter(explode(',', $s['slides']))), + 'durations' => (object)self::durations($s), 'scarto_target' => (float)str_replace(',', '.', $s['scarto_target']), 'city' => $s['city'], 'lat' => $s['lat'], diff --git a/public/userarea/kiosk.php b/public/userarea/kiosk.php index 6ecfd866..1c33c858 100644 --- a/public/userarea/kiosk.php +++ b/public/userarea/kiosk.php @@ -972,6 +972,173 @@ if (($_GET['a'] ?? '') === 'data') { margin-right: .8rem; } + /* meteo */ + .wxs { + flex: 1; + display: grid; + grid-template-columns: 34rem minmax(0, 1fr); + gap: 1.4rem; + min-height: 0; + } + + .wnow { + padding: 2rem 2.2rem; + display: flex; + flex-direction: column; + background: radial-gradient(600px 300px at 30% 20%, rgba(252, 211, 77, .14), transparent 70%), var(--panel); + } + + .wbig { + display: flex; + align-items: center; + gap: 1.4rem; + margin-top: 1rem; + } + + .wbig .wi { + font-size: 8rem; + line-height: 1; + } + + .wbig span { + font-size: 8.5rem; + font-weight: 900; + line-height: 1; + } + + .wdesc { + font-size: 2.2rem; + font-weight: 800; + margin-top: .8rem; + } + + .wmeta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem 1.4rem; + margin-top: auto; + padding-top: 1.4rem; + } + + .wmeta span { + display: block; + font-size: 1rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--label); + } + + .wmeta b { + font-size: 1.9rem; + font-weight: 900; + } + + .wright { + display: flex; + flex-direction: column; + gap: 1.4rem; + min-height: 0; + } + + .whours { + padding: 1.4rem 1.8rem; + } + + .hrow { + display: grid; + grid-template-columns: repeat(8, minmax(0, 1fr)); + gap: .8rem; + margin-top: 1rem; + } + + .hc { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: .3rem; + } + + .hc .t { + font-size: 1.3rem; + font-weight: 800; + color: var(--label); + } + + .hc .wi { + font-size: 2.8rem; + } + + .hc b { + font-size: 2.2rem; + font-weight: 900; + } + + .hc .r, + .dc .dr { + font-size: 1.2rem; + font-weight: 800; + color: var(--muted); + } + + .hi { + color: #60a5fa !important; + } + + .mid { + color: #93c5fd !important; + } + + .wdays { + flex: 1; + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 1.2rem; + min-height: 0; + } + + .dc { + min-width: 0; + padding: 1.4rem 1rem; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: .4rem; + } + + .dc .dn { + white-space: nowrap; + font-size: 1.7rem; + font-weight: 900; + } + + .dc .wi { + font-size: 4rem; + margin: .4rem 0; + } + + .dc .dt b { + font-size: 2.6rem; + font-weight: 900; + } + + .dc .dt span { + font-size: 1.8rem; + font-weight: 800; + color: var(--muted); + margin-left: .6rem; + } + + .dc .dd { + font-size: 1.1rem; + font-weight: 700; + color: var(--label); + min-height: 2.8rem; + } + .empty { margin: auto; color: var(--muted); @@ -1101,6 +1268,12 @@ if (($_GET['a'] ?? '') === 'data') { i: 'bi-list-ol', f: slideQueue }); + if (has('meteo') && WX && WX.daily && WX.hourly) S.push({ + k: 'meteo', + t: 'Meteo' + (D.settings.city ? ' · ' + D.settings.city : ''), + i: 'bi-cloud-sun-fill', + f: slideWeather + }); if (has('sicurezza') && D.safety) S.push({ k: 'sicurezza', t: 'Sicurezza', @@ -1152,7 +1325,8 @@ if (($_GET['a'] ?? '') === 'data') { return setTimeout(next, 10000); } show(seq[pos]); - const sec = D.settings.slide_seconds * (seq[pos].k === 'comunicazioni' || seq[pos].k === 'turni' || seq[pos].k === 'menu' ? 1.3 : 1); + // durata impostata per questo tipo di slide, altrimenti quella predefinita (un po' di più per i testi) + const sec = (D.settings.durations || {})[seq[pos].k] || D.settings.slide_seconds * (seq[pos].k === 'comunicazioni' || seq[pos].k === 'turni' || seq[pos].k === 'menu' ? 1.3 : 1); const p = $('#prog'); p.style.transition = 'none'; p.style.width = '0'; @@ -1461,18 +1635,116 @@ if (($_GET['a'] ?? '') === 'data') { setInterval(weather, 30 * 60000); } + // meteo: ora, prossime ore e prossimi giorni (open-meteo, gratuito e senza chiave) + let WX = null; + const WX_TXT = { + 0: 'Sereno', + 1: 'Quasi sereno', + 2: 'Parzialmente nuvoloso', + 3: 'Coperto', + 45: 'Nebbia', + 48: 'Nebbia con brina', + 51: 'Pioviggine leggera', + 53: 'Pioviggine', + 55: 'Pioviggine intensa', + 56: 'Pioviggine gelata', + 57: 'Pioviggine gelata', + 61: 'Pioggia debole', + 63: 'Pioggia', + 65: 'Pioggia forte', + 66: 'Pioggia gelata', + 67: 'Pioggia gelata forte', + 71: 'Neve debole', + 73: 'Neve', + 75: 'Neve forte', + 77: 'Nevischio', + 80: 'Rovesci leggeri', + 81: 'Rovesci', + 82: 'Rovesci violenti', + 85: 'Rovesci di neve', + 86: 'Rovesci di neve forti', + 95: 'Temporale', + 96: 'Temporale con grandine', + 99: 'Temporale con grandine' + }; + + function wxIcon(w, day = true) { + w = +w; + if (w === 0) return day ? ['bi-sun-fill', '#fcd34d'] : ['bi-moon-stars-fill', '#c7d2fe']; + if (w <= 2) return day ? ['bi-cloud-sun-fill', '#fcd34d'] : ['bi-cloud-moon-fill', '#c7d2fe']; + if (w === 3) return ['bi-cloud-fill', '#cbd5e1']; + if (w <= 48) return ['bi-cloud-fog2-fill', '#94a3b8']; + if (w <= 57) return ['bi-cloud-drizzle-fill', '#7dd3fc']; + if (w <= 67) return ['bi-cloud-rain-fill', '#60a5fa']; + if (w <= 77) return ['bi-snow', '#e0f2fe']; + if (w <= 82) return ['bi-cloud-rain-heavy-fill', '#3b82f6']; + if (w <= 86) return ['bi-cloud-snow-fill', '#e0f2fe']; + return ['bi-cloud-lightning-rain-fill', '#a78bfa']; + } + const ico = (w, day, cls = '') => { + const [i, c] = wxIcon(w, day); + return ``; + }; + function weather() { const s = D.settings; - fetch(`https://api.open-meteo.com/v1/forecast?latitude=${encodeURIComponent(s.lat)}&longitude=${encodeURIComponent(s.lon)}¤t=temperature_2m,weather_code&timezone=auto`) + const q = `latitude=${encodeURIComponent(s.lat)}&longitude=${encodeURIComponent(s.lon)}&timezone=Europe%2FRome&forecast_days=7` + + '¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code,is_day' + + '&hourly=temperature_2m,weather_code,precipitation_probability,is_day' + + '&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max,sunrise,sunset'; + fetch('https://api.open-meteo.com/v1/forecast?' + q) .then(r => r.json()).then(r => { const c = r.current; if (!c) return; - const w = +c.weather_code; - const ic = w === 0 ? 'bi-sun-fill' : w <= 2 ? 'bi-cloud-sun-fill' : w === 3 ? 'bi-cloud-fill' : w <= 48 ? 'bi-cloud-fog2-fill' : w <= 67 ? 'bi-cloud-rain-fill' : w <= 77 ? 'bi-snow' : w <= 82 ? 'bi-cloud-rain-heavy-fill' : 'bi-cloud-lightning-rain-fill'; - $('#wx').innerHTML = `