feat(wetter): Wetter auf dem Schreibtisch und in der Leiste
Zwei Schalter, unabhaengig voneinander: Schreibtisch, Leiste, beides oder keins. Die Stadt wird einmal gesucht und mit ihren Koordinaten gespeichert, sonst suchte jede Abfrage den Ort erneut. Geholt wird serverseitig und zehn Minuten zwischengespeichert. Der Grund ist nicht Sparsamkeit: Die Leiste steht auf jeder Seite jeder App, und fragte sie selbst an, ginge bei jedem Seitenaufruf eine Anfrage samt IP an einen Fremden — und die CSP jeder App muesste ihn erlauben. Kein Ort eingestellt oder Dienst nicht erreichbar: 204, und das Symbol bleibt weg. Eine Zahl von gestern waere schlechter als keine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
52b11efe06
commit
4e38ccdec1
10 files changed
+823
-6
No files matched your search
@@ -26,11 +26,20 @@ const DEFAULT_SETTINGS = {
|
||||
flicker: "on",
|
||||
glow: "on",
|
||||
openIn: "tab", // tab | same
|
||||
// Wetter. Zwei Schalter statt einem: Der Schreibtisch und die Leiste sind
|
||||
// zwei verschiedene Orte, und wer das Wetter beim Arbeiten in einer App will,
|
||||
// will es nicht zwingend auch auf einer Seite, die er selten aufruft.
|
||||
weatherBar: "off",
|
||||
weatherDesk: "off",
|
||||
weatherCity: "",
|
||||
weatherLat: "",
|
||||
weatherLon: "",
|
||||
};
|
||||
|
||||
const state = {
|
||||
cfg: { user: "", hostname: "honeyos", version: "…", apps: [] },
|
||||
me: null, // {sub, name, picture} sobald angemeldet
|
||||
weather: null, // zuletzt geholtes Wetter, oder null
|
||||
oidc: false,
|
||||
settings: { ...DEFAULT_SETTINGS },
|
||||
probes: {}, // appId -> "up" | "down" | "unknown"
|
||||
@@ -47,6 +56,107 @@ const isTouch = () => window.matchMedia("(pointer: coarse)").matches;
|
||||
* im Retro-Raster statt aus einer Icon-Bibliothek zu kommen.
|
||||
*/
|
||||
const GLYPHS = {
|
||||
// Wettersymbole. Dieselben Namen wie in weather.go (wmo) und in der Leiste —
|
||||
// solange 1.3 (geteilte Symbolquelle) nicht gebaut ist, stehen sie zweimal da.
|
||||
sonne: [
|
||||
"............",
|
||||
".....##.....",
|
||||
"..#..##..#..",
|
||||
"...#....#...",
|
||||
"....####....",
|
||||
"..##....##..",
|
||||
"..##....##..",
|
||||
"....####....",
|
||||
"...#....#...",
|
||||
"..#..##..#..",
|
||||
".....##.....",
|
||||
"............",
|
||||
],
|
||||
heiter: [
|
||||
"............",
|
||||
"...##.......",
|
||||
"..####..#...",
|
||||
"...##..###..",
|
||||
"......#####.",
|
||||
"...####...#.",
|
||||
"..#....##.#.",
|
||||
".##........#",
|
||||
"#..........#",
|
||||
".##########.",
|
||||
"............",
|
||||
"............",
|
||||
],
|
||||
wolke: [
|
||||
"............",
|
||||
"....####....",
|
||||
"...#....#...",
|
||||
"..#......##.",
|
||||
".##........#",
|
||||
"##.........#",
|
||||
"#..........#",
|
||||
"#..........#",
|
||||
".##########.",
|
||||
"............",
|
||||
"............",
|
||||
"............",
|
||||
],
|
||||
nebel: [
|
||||
"............",
|
||||
"....####....",
|
||||
"..##....##..",
|
||||
".##.......#.",
|
||||
"#..........#",
|
||||
".##########.",
|
||||
"............",
|
||||
".#########..",
|
||||
"............",
|
||||
"..#########.",
|
||||
"............",
|
||||
".#########..",
|
||||
],
|
||||
regen: [
|
||||
"............",
|
||||
"....####....",
|
||||
"...#....#...",
|
||||
"..#......##.",
|
||||
".##........#",
|
||||
"##.........#",
|
||||
".##########.",
|
||||
"............",
|
||||
"..#..#..#...",
|
||||
".#..#..#....",
|
||||
"..#..#..#...",
|
||||
".#..#..#....",
|
||||
],
|
||||
schnee: [
|
||||
"............",
|
||||
"....####....",
|
||||
"...#....#...",
|
||||
"..#......##.",
|
||||
".##........#",
|
||||
"##.........#",
|
||||
".##########.",
|
||||
"............",
|
||||
"..#...#...#.",
|
||||
".#.#.#.#.#..",
|
||||
"..#...#...#.",
|
||||
"............",
|
||||
],
|
||||
gewitter: [
|
||||
"............",
|
||||
"....####....",
|
||||
"...#....#...",
|
||||
"..#......##.",
|
||||
".##........#",
|
||||
"##.........#",
|
||||
".##########.",
|
||||
"......##....",
|
||||
".....##.....",
|
||||
"....####....",
|
||||
"......##....",
|
||||
".....##.....",
|
||||
],
|
||||
|
||||
cloud: [
|
||||
"............",
|
||||
"....####....",
|
||||
@@ -346,6 +456,11 @@ function renderShell() {
|
||||
renderMenubarApps();
|
||||
renderMode();
|
||||
tickClock();
|
||||
// Nicht abgewartet: Das Wetter kommt von außen und darf den Aufbau des
|
||||
// Schreibtischs nicht verzögern. Danach alle 10 Minuten, im selben Takt,
|
||||
// in dem der Server seinen Zwischenspeicher erneuert.
|
||||
loadWeather();
|
||||
setInterval(loadWeather, 10 * 60 * 1000);
|
||||
}
|
||||
|
||||
function renderMenubarApps() {
|
||||
@@ -383,9 +498,57 @@ function renderMode() {
|
||||
renderTiles();
|
||||
}
|
||||
renderStats();
|
||||
renderWeather();
|
||||
paintProbes();
|
||||
}
|
||||
|
||||
/* ── Wetter ────────────────────────────────────────────────────────────────
|
||||
*
|
||||
* Der Server holt es und legt es zehn Minuten beiseite; hier wird nur gezeigt,
|
||||
* was er hergibt. Kein Ort eingestellt oder Dienst nicht erreichbar: 204, und
|
||||
* das Widget bleibt weg. Eine Zahl von gestern wäre schlechter als keine.
|
||||
*/
|
||||
async function loadWeather() {
|
||||
if (!state.settings.weatherCity) {
|
||||
state.weather = null;
|
||||
renderWeather();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
state.weather = await api("/api/weather");
|
||||
} catch {
|
||||
state.weather = null;
|
||||
}
|
||||
renderWeather();
|
||||
}
|
||||
|
||||
function weatherHtml(w) {
|
||||
return `
|
||||
<div class="wx">
|
||||
<span class="wx-glyph">${glyphSvg(w.glyph)}</span>
|
||||
<div class="wx-text">
|
||||
<b class="wx-temp">${Math.round(w.grad)}°</b>
|
||||
<span class="wx-desc">${w.text}</span>
|
||||
<span class="wx-city">${w.ort}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderWeather() {
|
||||
const an = state.settings.weatherDesk === "on" && state.weather;
|
||||
const tile = $("wxTile");
|
||||
const desk = $("wxDesk");
|
||||
const desktopMode = state.settings.mode === "desktop";
|
||||
|
||||
tile.hidden = !an || desktopMode;
|
||||
desk.hidden = !an || !desktopMode;
|
||||
if (!an) return;
|
||||
|
||||
const html = weatherHtml(state.weather);
|
||||
tile.querySelector(".wx-body").innerHTML = html;
|
||||
desk.innerHTML = html;
|
||||
}
|
||||
|
||||
function setMode(mode) {
|
||||
if (state.settings.mode === mode) return;
|
||||
state.settings.mode = mode;
|
||||
@@ -875,6 +1038,19 @@ function openSettings() {
|
||||
${seg("openIn", [["tab", "neuer tab"], ["same", "gleiches fenster"]], s.openIn)}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="label">Wetter</span>
|
||||
${seg("weatherDesk", [["on", "auf dem schreibtisch"], ["off", "aus"]], s.weatherDesk)}
|
||||
<div style="height:6px"></div>
|
||||
${seg("weatherBar", [["on", "in der leiste"], ["off", "aus"]], s.weatherBar)}
|
||||
<div class="wx-city-row">
|
||||
<input id="wxQuery" type="text" placeholder="Stadt suchen …" autocomplete="off" />
|
||||
<button class="btn" id="btnWxSearch">suchen</button>
|
||||
</div>
|
||||
<div id="wxResults" class="wx-results"></div>
|
||||
<p class="ae-hint" id="wxCurrent" style="margin-left:0"></p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="label">Programme</span>
|
||||
<button class="btn" id="btnApps">${state.cfg.apps.length} eingetragen — verwalten …</button>
|
||||
@@ -902,10 +1078,58 @@ function openSettings() {
|
||||
state.settings[key] = btn.dataset.v;
|
||||
saveSettings();
|
||||
renderStats();
|
||||
if (key.startsWith("weather")) loadWeather();
|
||||
if (key === "openIn" && state.settings.mode === "terminal") renderTiles();
|
||||
});
|
||||
});
|
||||
|
||||
const zeigeOrt = () => {
|
||||
root.querySelector("#wxCurrent").textContent = state.settings.weatherCity
|
||||
? `eingestellt: ${state.settings.weatherCity}`
|
||||
: "kein Ort eingestellt — ohne Ort bleibt das Wetter aus.";
|
||||
};
|
||||
zeigeOrt();
|
||||
|
||||
const suchen = async () => {
|
||||
const q = root.querySelector("#wxQuery").value.trim();
|
||||
const out = root.querySelector("#wxResults");
|
||||
if (!q) return;
|
||||
out.textContent = "suche …";
|
||||
let orte;
|
||||
try {
|
||||
orte = await api("/api/weather/search?q=" + encodeURIComponent(q));
|
||||
} catch {
|
||||
out.textContent = "Ortssuche nicht erreichbar.";
|
||||
return;
|
||||
}
|
||||
if (!orte || !orte.length) {
|
||||
out.textContent = "nichts gefunden.";
|
||||
return;
|
||||
}
|
||||
out.innerHTML = orte
|
||||
.map((o, i) => `<button type="button" class="wx-hit" data-i="${i}">${o.name}<span>${o.land}</span></button>`)
|
||||
.join("");
|
||||
out.querySelectorAll(".wx-hit").forEach((b) =>
|
||||
b.addEventListener("click", () => {
|
||||
const o = orte[Number(b.dataset.i)];
|
||||
// Koordinaten mitspeichern: Sonst müsste jede Wetterabfrage den Ort
|
||||
// erneut suchen — zwei Anfragen nach draußen statt einer.
|
||||
state.settings.weatherCity = o.name;
|
||||
state.settings.weatherLat = String(o.lat);
|
||||
state.settings.weatherLon = String(o.lon);
|
||||
saveSettings();
|
||||
out.innerHTML = "";
|
||||
root.querySelector("#wxQuery").value = "";
|
||||
zeigeOrt();
|
||||
loadWeather();
|
||||
})
|
||||
);
|
||||
};
|
||||
root.querySelector("#btnWxSearch").addEventListener("click", suchen);
|
||||
root.querySelector("#wxQuery").addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") { e.preventDefault(); suchen(); }
|
||||
});
|
||||
|
||||
root.querySelector("#btnApps").addEventListener("click", openApps);
|
||||
},
|
||||
420
|
||||
|
||||
Reference in new issue
Block a user