Titel-Mockup getauscht, Projektstand geht beim Laden nicht mehr verloren
Der Ladepfad hat jeden Stand ab Version 3 verworfen: STATE_V stand auf 3, die Pruefung liess aber nur 1 und 2 durch. Damit lief das Blatt mit der Voreinstellung an und schrieb sie ueber die vorhandene Datei. Die Pruefung gilt jetzt von 1 bis STATE_V. Damit so etwas nicht mehr unbemerkt bleibt: liegt eine Standdatei vor, die sich nicht anwenden laesst, wird nichts mehr zurueckgeschrieben, weder in die Datei noch in den localStorage. Merkzettel gehaertet. "Stand merken" liest die Datei gegen und meldet erst dann Erfolg. "Zurueck zum Stand" prueft Version und Slide-Anzahl, bevor es den laufenden Stand ersetzt, legt ihn vorher als <projekt>-vorher ab und laedt erst neu, wenn die Datei nachweislich steht. Auf der Titel-Slide steht das Geraet jetzt auf dem Fels-Mockup. Die Wortmarke steckt im Bild, deshalb faellt die separate wabe-logo dort weg. Der Rahmen ist auf 880px breiter, weil das Bild links und rechts rund ein Viertel freien Rand mitbringt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
25ffc30c4e
commit
344f58dde0
2 files changed
+100
-24
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
+100
-24
@@ -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/<projekt>.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"]
|
||||
? '<div class="szene"><img class="szene-dev" alt="" src="' +
|
||||
assets["./assets/mockups/macbook-board.png"] + '"></div>'
|
||||
: '<div class="laptop-missing">Mockup fehlt.<br>Das freigestellte PNG als <b>assets/mockups/macbook-board.png</b> ablegen.</div>';
|
||||
// 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"]
|
||||
? '<div class="szene"><img class="szene-dev" alt="HiveGrid" src="' +
|
||||
assets["./assets/mockups/macbook-fels.png"] + '"></div>'
|
||||
: '<div class="laptop-missing">Mockup fehlt.<br>Das freigestellte PNG als <b>assets/mockups/macbook-fels.png</b> ablegen.</div>';
|
||||
return '<div class="title-top">' +
|
||||
'<p class="wabe-tagline">Vom Handwerker<br>für <span class="a">Handwerker</span></p>' +
|
||||
'<img class="wabe-logo" alt="HiveGrid" src="' + logo + '">' +
|
||||
'</div>' + dev +
|
||||
'<p class="wabe-claim">' + s.sub + '</p>';
|
||||
}
|
||||
@@ -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 <projekt>-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");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user