/* ===================================================================== material.js — Bilder und Videos zum Posten, jedes nur einmal ===================================================================== Auftrag vom 22.09.2026. DIESE DATEI KENNT KEINEN ROLLENNAMEN. Ob jemand einstellen darf und ob er sieht, wer sich etwas genommen hat, sagt der Server (`darf_einstellen`, `sieht_wer`). Stünde es hier, stünde es in einer Datei, die jeder herunterlädt — und wäre am Tag der nächsten Änderung falsch. DER WICHTIGSTE TEIL IST DAS NEHMEN, und es sind zwei Schritte: erst festschreiben (`/nehmen`), dann herunterladen. Wer zu spät kommt, bekommt 409 und sieht sofort, dass es weg ist — statt eine Datei zu laden, die schon jemand anders postet. ===================================================================== */ (() => { 'use strict'; const $ = (id) => document.getElementById(id); const el = (was, klasse, text) => { const e = document.createElement(was); if (klasse) e.className = klasse; if (text !== undefined) e.textContent = text; return e; }; const melde = (t) => { const f = $('fehler'); if (f) f.textContent = t || ''; }; let stand = null; /* Eine Größe, die ein Mensch lesen kann. */ function groesse(bytes) { const mb = bytes / (1024 * 1024); if (mb >= 1) return `${mb.toFixed(1).replace('.', ',')} MB`; return `${Math.max(1, Math.round(bytes / 1024))} KB`; } function datum(roh) { try { const d = new Date(roh); return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); } catch { return ''; } } /* EIN PUNKT, ABER KEIN ZWEITER. Eine deutsche Datumsangabe endet selbst auf einen Punkt ("22.09."). Ein angehängter Satzpunkt ergibt dann "am 22.09.." — auf dem Bildschirm sieht das nach Tippfehler aus. Gefunden beim ersten Blick mit echten Augen, nicht in einer Messung: Kein Prüfprogramm hätte danach gefragt. */ const punkt = (t) => (/[.!?]$/.test(t) ? t : t + '.'); /* =================================================================== EINE KARTE =================================================================== */ function karte(m) { const k = el('article', 'm-karte'); k.dataset.id = String(m.id); if (m.genommen) k.dataset.genommen = 'ja'; /* DIE VORSCHAU IST DIE HAUPTSACHE. Wer entscheiden soll, ob er etwas postet, muss es sehen -- ein Dateiname reicht dafür nicht. SIE LÄDT ERST BEIM SICHTBARWERDEN (`loading="lazy"`): Bei dreißig Bildern wären das sonst dreißig Abrufe auf einmal, und am Handy im Funkloch sieht man eine Minute lang nichts. */ const schau = el('div', 'm-karte__schau'); if (m.genommen) { /* Was weg ist, wird nicht mehr ausgeliefert -- der Server gibt die Datei nur dem, der sie genommen hat. Statt eines kaputten Bildes steht hier ein ehrliches Feld. */ schau.append(el('span', 'm-karte__weg-zeichen', m.art === 'video' ? '▣' : '▤')); } else if (m.art === 'bild') { const bild = el('img', 'm-karte__bild'); bild.src = `/workspace/api/material/${m.id}/vorschau`; bild.alt = ''; bild.loading = 'lazy'; bild.decoding = 'async'; schau.append(bild); } else { const v = el('span', 'm-karte__art', 'Video'); schau.append(v); } k.append(schau); const koerper = el('div', 'm-karte__koerper'); koerper.append(el('p', 'm-karte__name', m.name)); if (m.text) koerper.append(el('p', 'm-karte__text', m.text)); const fuss = el('p', 'm-karte__fuss'); fuss.append(el('span', 'm-karte__marke', m.art === 'video' ? 'Video' : 'Bild')); fuss.append(el('span', 'm-karte__leise', groesse(m.groesse))); fuss.append(el('span', 'm-karte__leise', `von ${m.von}`)); koerper.append(fuss); /* ---- Der Zustand ---- */ const meldung = el('p', 'm-karte__meldung'); meldung.hidden = true; if (m.genommen) { /* WER ES HAT, STEHT NUR FÜR ZWEI DA. Der Server schickt das Feld den anderen gar nicht erst mit -- diese Zeile prüft deshalb nicht auf eine Rolle, sondern darauf, ob die Angabe da ist. */ if (m.von_mir_genommen) { koerper.append(el('p', 'm-karte__stand m-karte__stand--meins', 'Du hast es genommen.')); const a = el('a', 'knopf knopf--klein', 'Noch einmal laden'); a.href = `/workspace/api/material/${m.id}/datei`; a.setAttribute('download', m.name); koerper.append(a); } else if (m.genommen_von) { koerper.append(el('p', 'm-karte__stand', punkt( `${m.genommen_von} hat es genommen${m.genommen_am ? ' am ' + datum(m.genommen_am) : ''}`))); } else { koerper.append(el('p', 'm-karte__stand', 'Schon benutzt.')); } } else { const knopf = el('button', 'knopf knopf--klein', 'Nehmen und laden'); knopf.type = 'button'; knopf.addEventListener('click', () => nehmen(m, knopf, meldung)); koerper.append(knopf); } koerper.append(meldung); k.append(koerper); return k; } /* =================================================================== NEHMEN — erst festschreiben, dann laden =================================================================== */ async function nehmen(m, knopf, meldung) { knopf.disabled = true; knopf.textContent = 'einen Moment …'; try { const a = await fetch(`/workspace/api/material/${m.id}/nehmen`, { method: 'POST' }); if (a.status === 409) { /* DER WICHTIGE FALL: Jemand war schneller. Kein Fehler, keine rote Zeile -- eine Auskunft. Und die Liste wird neu geholt, damit man sofort sieht, was noch da ist. */ const j = await a.json().catch(() => ({})); meldung.textContent = j.wer ? `${j.wer} war ein paar Sekunden schneller.` : 'Da war jemand schneller — das ist schon vergeben.'; meldung.hidden = false; knopf.remove(); setTimeout(laden, 1400); return; } if (!a.ok) { const j = await a.json().catch(() => ({})); meldung.textContent = window.sagWas?.(j.fehler, 'Ging gerade nicht.') || 'Ging gerade nicht.'; meldung.hidden = false; knopf.disabled = false; knopf.textContent = 'Nehmen und laden'; return; } /* ES GEHÖRT JETZT IHM. Der Download wird ausgelöst, ohne die Seite zu verlassen -- ein `location.assign` hätte die Liste weggeworfen, und danach wüsste niemand mehr, was er gerade geholt hat. */ const weg = (await a.json().catch(() => ({}))).weg || `/workspace/api/material/${m.id}/datei`; const link = document.createElement('a'); link.href = weg; link.download = m.name; document.body.append(link); link.click(); link.remove(); meldung.textContent = 'Gehört dir. Der Download läuft.'; meldung.hidden = false; knopf.remove(); setTimeout(laden, 1200); } catch { meldung.textContent = 'Das Netz war gerade weg. Versuch es noch einmal.'; meldung.hidden = false; knopf.disabled = false; knopf.textContent = 'Nehmen und laden'; } } /* =================================================================== EINSTELLEN =================================================================== */ function einstellenAufbauen() { const knopf = $('m-hoch'); if (!knopf) return; knopf.addEventListener('click', async () => { const feld = $('m-datei'); const datei = feld?.files?.[0]; const stand2 = $('m-stand'); if (!datei) { stand2.textContent = 'Such erst ein Bild oder Video aus.'; return; } if (stand?.max_bytes && datei.size > stand.max_bytes) { stand2.textContent = `Das ist zu groß (${groesse(datei.size)}). ` + `Mehr als ${groesse(stand.max_bytes)} geht nicht.`; return; } knopf.disabled = true; /* ÜBER DEN GEMEINSAMEN HOCHLADER -- er zeigt den Fortschritt und lässt sich abbrechen. Genau dafür wurde er am 21.09. gebaut; eine eigene Fassung hier wäre die zweite, die es nicht kann. DIE SCHNITTSTELLE IST NACHGELESEN, NICHT GERATEN: `hochladen()` nimmt (Pfad, Daten, {kopf, beiFortschritt}) und liefert {fertig, abbrechen}; `fertig` löst mit {ok, status, daten} auf. Mein erster Entwurf hatte sie sich ausgedacht. */ const anzeige = window.hochladeAnzeige?.($('m-fortschritt'), datei.name, () => lauf?.abbrechen()); const lauf = window.hochladen?.('/workspace/api/material', datei, { kopf: { 'Content-Type': datei.type || 'application/octet-stream', 'X-Datei-Name': encodeURIComponent(datei.name || ''), 'X-Text': encodeURIComponent(($('m-text')?.value || '').slice(0, 600)), }, beiFortschritt: (anteil, gesendet, gesamt) => anzeige?.fortschritt(anteil, gesendet, gesamt), beiVerarbeitung: () => anzeige?.verarbeiten(), }); try { const ergebnis = lauf ? await lauf.fertig : { ok: false, netz: true }; anzeige?.weg(); if (ergebnis.abgebrochen) { stand2.textContent = 'Abgebrochen. Es wurde nichts bereitgestellt.'; return; } if (!ergebnis.ok) { stand2.textContent = ergebnis.netz ? 'Das Netz war gerade weg. Versuch es noch einmal.' : (window.sagWas?.(ergebnis.daten?.fehler, 'Ging nicht.') || 'Ging nicht.'); return; } stand2.textContent = 'Steht jetzt bereit.'; feld.value = ''; const t = $('m-text'); if (t) t.value = ''; await laden(); } finally { knopf.disabled = false; } }); } /* =================================================================== LADEN =================================================================== */ async function laden() { try { const a = await fetch('/workspace/api/material'); if (!a.ok) { window.ladefehler?.({ nochmal: laden }); return; } stand = await a.json(); $('neu-block').hidden = !stand.darf_einstellen; const frei = (stand.material || []).filter((m) => !m.genommen); const weg = (stand.material || []).filter((m) => m.genommen); const zielFrei = $('frei'); zielFrei.replaceChildren(); zielFrei.setAttribute('aria-busy', 'false'); if (!frei.length) { /* KEIN LEERER BILDSCHIRM. Ob das gut oder schlecht ist, hängt davon ab, wer liest -- deshalb zwei Sätze. */ zielFrei.append(el('p', 'm-leer', stand.darf_einstellen ? 'Gerade nichts frei. Stell oben etwas bereit — dann können die anderen es sich holen.' : 'Gerade ist nichts frei. Schau später noch einmal rein — hier kommt regelmäßig Neues dazu.')); } for (const m of frei) zielFrei.append(karte(m)); $('frei-satz').textContent = frei.length ? `${frei.length} ${frei.length === 1 ? 'Stück' : 'Stück'} zum Mitnehmen. ` + 'Wer es nimmt, hat es — danach ist es für alle anderen weg.' : ''; $('weg-block').hidden = !weg.length; const zielWeg = $('weg'); zielWeg.replaceChildren(); for (const m of weg) zielWeg.append(karte(m)); if (weg.length) { $('weg-satz').textContent = stand.sieht_wer ? `${weg.length} schon vergeben — mit Namen, damit du weißt, wer was postet.` : `${weg.length} schon vergeben. Damit nichts zweimal gepostet wird.`; } $('laedt').hidden = true; $('haupt')?.removeAttribute('aria-busy'); melde(''); } catch { window.ladefehler?.({ nochmal: laden }); } } einstellenAufbauen(); laden(); })();