update kiosk

This commit is contained in:
2026-10-21 12:24:12 +02:00
parent 5444ea943a
commit d6d424cae8
3 changed files with 320 additions and 9 deletions
+277 -5
View File
@@ -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 `<i class="bi ${i} wi ${cls}" style="color:${c}"></i>`;
};
function weather() {
const s = D.settings;
fetch(`https://api.open-meteo.com/v1/forecast?latitude=${encodeURIComponent(s.lat)}&longitude=${encodeURIComponent(s.lon)}&current=temperature_2m,weather_code&timezone=auto`)
const q = `latitude=${encodeURIComponent(s.lat)}&longitude=${encodeURIComponent(s.lon)}&timezone=Europe%2FRome&forecast_days=7` +
'&current=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 = `<i class="bi ${ic}"></i><div>${Math.round(c.temperature_2m)}°<small>${esc(s.city || '')}</small></div>`;
WX = r;
$('#wx').innerHTML = `${ico(c.weather_code, c.is_day !== 0)}<div>${Math.round(c.temperature_2m)}°<small>${esc(s.city || '')}</small></div>`;
}).catch(() => {});
}
function slideWeather() {
const c = WX.current,
d = WX.daily,
h = WX.hourly;
const hhmm = t => String(t).slice(11, 16);
// prossime 8 ore a partire dall'ora attuale
const nowH = String(c.time).slice(0, 13);
let i0 = h.time.findIndex(t => t.slice(0, 13) >= nowH);
if (i0 < 0) i0 = 0;
const hours = [];
for (let i = i0 + 1; i < h.time.length && hours.length < 8; i++) hours.push(i);
const GG = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
const days = d.time.map((t, i) => i).slice(1, 7);
const rainCls = p => p >= 60 ? 'hi' : (p >= 30 ? 'mid' : '');
return `<div class="wxs">
<div class="panel wnow">
<div class="lbl">Adesso${D.settings.city ? ' a ' + esc(D.settings.city) : ''}</div>
<div class="wbig">${ico(c.weather_code, c.is_day !== 0)}<span>${Math.round(c.temperature_2m)}°</span></div>
<div class="wdesc">${esc(WX_TXT[c.weather_code] || '')}</div>
<div class="wmeta">
<div><span>Percepita</span><b>${Math.round(c.apparent_temperature)}°</b></div>
<div><span>Min / max</span><b>${Math.round(d.temperature_2m_min[0])}° / ${Math.round(d.temperature_2m_max[0])}°</b></div>
<div><span>Umidità</span><b>${Math.round(c.relative_humidity_2m)}%</b></div>
<div><span>Vento</span><b>${Math.round(c.wind_speed_10m)} km/h</b></div>
<div><span>Pioggia oggi</span><b>${d.precipitation_probability_max[0] ?? 0}%</b></div>
<div><span>Alba · tramonto</span><b>${hhmm(d.sunrise[0])} · ${hhmm(d.sunset[0])}</b></div>
</div>
</div>
<div class="wright">
<div class="panel whours">
<div class="lbl">Prossime ore</div>
<div class="hrow">${hours.map(i => `<div class="hc"><span class="t">${hhmm(h.time[i])}</span>${ico(h.weather_code[i], h.is_day[i] !== 0)}<b>${Math.round(h.temperature_2m[i])}°</b><span class="r ${rainCls(h.precipitation_probability[i])}"><i class="bi bi-droplet-fill"></i> ${h.precipitation_probability[i] ?? 0}%</span></div>`).join('')}</div>
</div>
<div class="wdays">${days.map(i => {
const dt = new Date(d.time[i] + 'T12:00:00');
return `<div class="panel dc"><div class="dn">${i === 1 ? 'Domani' : GG[dt.getDay()] + ' ' + dt.getDate()}</div>${ico(d.weather_code[i], true)}
<div class="dt"><b>${Math.round(d.temperature_2m_max[i])}°</b><span>${Math.round(d.temperature_2m_min[i])}°</span></div>
<div class="dd">${esc(WX_TXT[d.weather_code[i]] || '')}</div>
<div class="dr ${rainCls(d.precipitation_probability_max[i])}"><i class="bi bi-droplet-fill"></i> ${d.precipitation_probability_max[i] ?? 0}%</div></div>`;
}).join('')}</div>
</div>
</div>`;
}
/* ---------- avvio ---------- */
tick();
load().then(() => next());