diff --git a/assets/mockups/macbook-fels.png b/assets/mockups/macbook-fels.png new file mode 100644 index 0000000..d457a6a Binary files /dev/null and b/assets/mockups/macbook-fels.png differ diff --git a/post1.html b/post1.html index aaf847b..dcc44dc 100644 --- a/post1.html +++ b/post1.html @@ -656,7 +656,11 @@ Display, keine Projektion per matrix3d, kein zweiter Screenshot dahinter. Damit fallen auch die gemessenen Vierecke und Eckradien weg, die das vorherige Szenen-Mockup gebraucht hat. */ - .szene { position: relative; width: 720px; } + /* Breiter als das vorherige Mockup, weil dieses Bild links und rechts rund + ein Viertel freien Rand mitbringt: bei 880px ist das Geraet selbst wieder + etwa so gross wie vorher, und 880 bleibt innerhalb der 888px, die zwischen + den Innenraendern der Slide frei sind. */ + .szene { position: relative; width: 880px; } .szene-dev { width: 100%; height: auto; display: block; } .laptop-missing { @@ -1061,7 +1065,7 @@ "logo-full": { label: "Logo", size: 560, prop: "width" }, "screen-logo": { label: "Logo im Screen", size: 430, prop: "width" }, "laptop": { label: "Laptop", size: 960, prop: "width" }, - "szene": { label: "Laptop · Board", size: 720, prop: "width" }, + "szene": { label: "Laptop · Board", size: 880, prop: "width" }, "mock": { label: "Bild", size: null, prop: "width" }, "mock-2": { label: "Zweites Bild", size: null, prop: "width" }, "note": { label: "Notizzettel", size: null, prop: "width" }, @@ -1148,7 +1152,22 @@ state/.json. Nur die Datei lässt sich pushen, also ist sie die Wahrheit; der localStorage ist der Rückfall, wenn der Server nicht läuft. */ let pushTimer = null; + + /* Ein vorhandener Stand, der sich nicht lesen laesst, ist ein Grund zum + Anhalten und nicht zum Weiterarbeiten: sonst laeuft das Blatt mit der + Voreinstellung an und schreibt die beim ersten Klick ueber genau die + Datei, die gerade nicht verstanden wurde. Solange die Sperre steht, geht + nichts mehr raus, weder in die Datei noch in den localStorage. */ + let stateLocked = false; + function lockState(grund) { + stateLocked = true; + clearTimeout(pushTimer); pushTimer = null; + setStatus("Stand nicht geladen (" + grund + "). Es wird nichts gespeichert, " + + "damit die vorhandene Datei nicht ueberschrieben wird.", "err"); + } + function saveState() { + if (stateLocked) return; const state = currentState(); try { localStorage.setItem(STORE, JSON.stringify(state)); } catch (err) { console.warn("Speichern fehlgeschlagen", err); } @@ -1159,6 +1178,7 @@ async function pushState(state) { pushTimer = null; + if (stateLocked) return; try { const res = await fetch("/state?project=" + PROJECT, { method: "POST", @@ -1171,24 +1191,34 @@ // Ein Reload darf keine Änderung verschlucken, die noch im Timer hängt. addEventListener("beforeunload", () => { - if (!pushTimer) return; + if (!pushTimer || stateLocked) return; clearTimeout(pushTimer); navigator.sendBeacon("/state?project=" + PROJECT, new Blob([JSON.stringify(currentState())], { type: "application/json" })); }); async function loadState() { - let d = null, fromFile = false; + let d = null, fromFile = false, fileSeen = false; // Erst die Datei aus dem Repo, dann der Browser. Wer das Projekt frisch // gezogen hat, soll den gepushten Stand sehen und nicht seinen eigenen alten. try { const res = await fetch(STATE_URL, { cache: "no-store" }); - if (res.ok) { d = await res.json(); fromFile = true; } + if (res.ok) { fileSeen = true; d = await res.json(); fromFile = true; } } catch (_) {} + // Datei ist da, laesst sich aber nicht lesen: anhalten statt ueberschreiben. + if (fileSeen && !d) { lockState("Datei unlesbar"); return false; } if (!d) { try { d = JSON.parse(localStorage.getItem(STORE) || "null"); } catch (_) {} } - if (!d || !(d.v === 1 || d.v === 2)) return false; + if (!d) return false; + // Jeder Stand von 1 bis zum aktuellen ist gueltig; aeltere hebt migrateState + // an. Frueher standen hier nur 1 und 2 fest verdrahtet, dadurch wurde jeder + // frisch gespeicherte Stand beim Laden verworfen und von der Voreinstellung + // ueberschrieben. + if (!(d.v >= 1 && d.v <= STATE_V)) { + if (fromFile) { lockState("Stand " + d.v + ", dieses Blatt kennt bis " + STATE_V); } + return false; + } const migrated = migrateState(d); // Stand aus dem Browser, aber noch keine Datei: einmalig hinueberschreiben, // sonst bliebe ein ueber Wochen gewachsener Stand ungeteilt liegen. @@ -1381,13 +1411,15 @@ } const logo = variant === "dunkel" ? assets["./assets/brand/logo-white.svg"] : assets["./assets/brand/logo.svg"]; if (layoutState[i] === "szene") { - const dev = assets["./assets/mockups/macbook-board.png"] - ? '
' - : '
Mockup fehlt.
Das freigestellte PNG als assets/mockups/macbook-board.png ablegen.
'; + // Die Wortmarke steckt in diesem Mockup schon im Bild. Ein zweites Logo + // auf der Slide waere eine Dopplung, deshalb faellt hier die wabe-logo + // weg und die Zeile oben traegt nur die Tagline. + const dev = assets["./assets/mockups/macbook-fels.png"] + ? '
HiveGrid
' + : '
Mockup fehlt.
Das freigestellte PNG als assets/mockups/macbook-fels.png ablegen.
'; return '
' + '

Vom Handwerker
für Handwerker

' + - '' + '
' + dev + '

' + s.sub + '

'; } @@ -1641,8 +1673,8 @@ .concat(SLIDES.filter(s => s.media2).map(s => s.media2.img)); paths.push("./assets/screens/pipelines.png", "./assets/screens/chat-tablet.png"); // Optional: the layout falls back to a hint if the file is not there yet. - const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-board.png"]); - paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-board.png"); + const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"]); + paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-fels.png"); for (const path of [...new Set(paths)]) { const res = await fetch(path, { cache: "no-store" }).catch(() => null); if (!res || !res.ok) { @@ -2394,26 +2426,56 @@ const snapSave = document.getElementById("snap-save"); const snapLoad = document.getElementById("snap-load"); + // Was aus der Merkdatei kommt, wird geprueft, bevor es den laufenden Stand + // ersetzt: Slide-Anzahl und Version muessen zu diesem Blatt passen. + function snapUsable(d) { + return !!d && typeof d.v === "number" && d.v >= 1 && d.v <= STATE_V + && Array.isArray(d.edits) && d.edits.length === SLIDES.length + && Array.isArray(d.layoutState) && d.layoutState.length === SLIDES.length + && Array.isArray(d.bgState) && d.bgState.length === SLIDES.length; + } + + // Immer frisch: der Merkzettel wird waehrend der Arbeit ueberschrieben, ein + // gecachter Treffer wuerde den Stand von vorhin zurueckholen. + function snapUrl() { + return "./state/" + SNAP_PROJECT + ".json?t=" + Date.now(); + } + + async function readSnap() { + const res = await fetch(snapUrl(), { cache: "no-store" }); + if (!res.ok) throw new Error("kein gemerkter Stand"); + return await res.json(); + } + async function snapExists() { - try { - const res = await fetch("./state/" + SNAP_PROJECT + ".json", { cache: "no-store" }); - return res.ok; - } catch (_) { return false; } + try { return snapUsable(await readSnap()); } + catch (_) { return false; } } snapExists().then(ok => { snapLoad.disabled = !ok; }); snapSave.addEventListener("click", async () => { snapSave.disabled = true; try { + const mine = currentState(); const res = await fetch("/state?project=" + SNAP_PROJECT, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(currentState()), + body: JSON.stringify(mine), }); if (!res.ok) throw new Error("HTTP " + res.status); + // Gegengelesen: gemerkt ist erst, was auch wirklich in der Datei steht. + // Ein "ok" vom Server allein hat hier schon einmal zu wenig bewiesen. + const back = await readSnap(); + if (!snapUsable(back)) throw new Error("Datei unbrauchbar zurueckgelesen"); + if (JSON.stringify(back.edits) !== JSON.stringify(mine.edits) + || JSON.stringify(back.bgState) !== JSON.stringify(mine.bgState) + || JSON.stringify(back.layoutState) !== JSON.stringify(mine.layoutState)) { + throw new Error("Datei weicht vom Stand ab"); + } snapLoad.disabled = false; - setStatus("Stand gemerkt", "ok"); + setStatus("Stand gemerkt und gegengelesen", "ok"); } catch (err) { + snapLoad.disabled = !(await snapExists()); setStatus("Merken fehlgeschlagen: " + err.message, "err"); } finally { snapSave.disabled = false; } }); @@ -2421,21 +2483,35 @@ snapLoad.addEventListener("click", async () => { snapLoad.disabled = true; try { - const res = await fetch("./state/" + SNAP_PROJECT + ".json", { cache: "no-store" }); - if (!res.ok) throw new Error("kein gemerkter Stand"); - const d = await res.json(); - // Erst den laufenden Timer entschaerfen, sonst schreibt er beim Entladen - // den alten Stand ueber den gerade zurueckgeholten. + const d = await readSnap(); + // Erst pruefen, dann ersetzen. Ein Merkzettel, der nicht passt, darf + // nicht den Stand kosten, der gerade auf dem Schirm steht. + if (!snapUsable(d)) throw new Error("gemerkter Stand passt nicht zu diesem Blatt"); + // Rueckfahrkarte: der Stand von jetzt wird als -vorher abgelegt, + // bevor er ueberschrieben wird. Ein Fehlklick ist damit umkehrbar. + await fetch("/state?project=" + PROJECT + "-vorher", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(currentState()), + }).catch(() => {}); + // Den laufenden Timer entschaerfen und dichtmachen, sonst schreibt der + // Reload den alten Stand ueber den gerade zurueckgeholten. clearTimeout(pushTimer); pushTimer = null; + stateLocked = true; const put = await fetch("/state?project=" + PROJECT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(d), }); if (!put.ok) throw new Error("HTTP " + put.status); + // Gegenlesen, bevor neu geladen wird: erst wenn die Datei steht, ist der + // Reload sicher. Sonst startet das Blatt ohne Stand. + const back = await fetch(STATE_URL + "?t=" + Date.now(), { cache: "no-store" }); + if (!back.ok || !snapUsable(await back.json())) throw new Error("Stand nicht geschrieben"); try { localStorage.setItem(STORE, JSON.stringify(d)); } catch (_) {} location.reload(); } catch (err) { + stateLocked = false; snapLoad.disabled = false; setStatus("Zurueckholen fehlgeschlagen: " + err.message, "err"); }