export function esc(wert: unknown): string { return String(wert ?? '').replace(/[&<>"']/g, z => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[z]!)); } /** Sekunden als 1:02:03 oder 2:03. */ export function dauer(sekunden: number | null): string { if (!sekunden || sekunden <= 0) return ''; const s = Math.round(sekunden); const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60); return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}` : `${m}:${String(s % 60).padStart(2, '0')}`; } export function groesse(bytes: number): string { if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`; if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MB`; return `${Math.round(bytes / 1024)} KB`; } /** Wie weit ein Stand gediehen ist, als Anteil — oder nichts. */ export function anteil(stand: { positionSeconds: number; runtimeSeconds: number | null } | null | undefined) : number | null { if (!stand?.runtimeSeconds || stand.runtimeSeconds <= 0) return null; return stand.positionSeconds / stand.runtimeSeconds; } /** „noch 42 Min." — was auf einer Kachel wirklich interessiert. */ export function verbleibend(stand: { positionSeconds: number; runtimeSeconds: number | null }): string { if (!stand.runtimeSeconds) return ''; const rest = Math.max(0, stand.runtimeSeconds - stand.positionSeconds); return rest < 60 ? 'fast durch' : `noch ${Math.round(rest / 60)} Min.`; } export function knopf(text: string, klasse = 'btn'): HTMLButtonElement { const b = document.createElement('button'); b.className = klasse; b.textContent = text; return b; } export function feld(platzhalter: string, art = 'text'): HTMLInputElement { const i = document.createElement('input'); i.className = 'feld'; i.type = art; i.placeholder = platzhalter; return i; } /** Zeigt einen Fehler, ohne die Seite zu verlassen. */ export function fehlerzeile(text: string): HTMLElement { const d = document.createElement('div'); d.className = 'fehler'; d.textContent = text; return d; } export function leer(text: string): HTMLElement { const d = document.createElement('div'); d.className = 'leer'; d.textContent = text; return d; } /** Was bei einem Fehler in der Oberfläche steht — immer deutsch. * * **Warum nicht einfach `fehler.message`.** Das stand überall, und überall * kam dabei Englisch heraus: „Failed to fetch" vom Browser, „An error * occurred while saving the entity changes" aus der Datenbankschicht. Ein * Server, der auf Deutsch antwortet, darf das nicht durchreichen. * * **Der Server schreibt seine Sätze selbst.** Kommt die Meldung von dort, * ist sie bereits deutsch und sagt, was zu tun ist — die wird übernommen. * Alles andere entsteht hier, weil es im Browser passiert ist und der * Server davon nichts weiss. */ export function meldung(fehler: unknown): string { // Eine Antwort des Servers: sein Text gilt. if (fehler instanceof Error && fehler.name === 'ApiFehler' && fehler.message) { return fehler.message; } // **Die Verbindung kam nicht zustande.** `fetch` wirft dafür einen // `TypeError` — und zwar in jedem Browser mit anderem Wortlaut: „Failed to // fetch", „Load failed", „NetworkError when attempting to fetch resource". if (fehler instanceof TypeError) { return 'Der Server war nicht erreichbar. Läuft er noch, und steht die Verbindung?'; } if (fehler instanceof DOMException && fehler.name === 'AbortError') { return 'Der Vorgang wurde abgebrochen.'; } const text = fehler instanceof Error ? fehler.message : String(fehler ?? ''); // Eine reine Statuszahl ist keine Meldung. if (/^\d{3}$/.test(text.trim())) { return `Der Server hat die Anfrage abgelehnt (${text.trim()}). ` + 'Die Einzelheiten stehen im Protokoll von heute.'; } // **Englisch bleibt draussen.** Was hier ankommt und keine deutschen // Umlaute oder typischen Wörter trägt, stammt fast sicher aus einer // Laufzeitbibliothek — das hilft niemandem weiter. if (text.length === 0 || !/[äöüßÄÖÜ]|\b(der|die|das|ist|wurde|nicht|kein|keine)\b/i.test(text)) { return 'Etwas ist schiefgegangen. Die Einzelheiten stehen im Protokoll von heute.'; } return text; } /** Eine kurze Meldung am unteren Rand, die von selbst wieder geht. * * **Warum es das braucht.** Eine Aktion aus einem Kontextmenü hat keinen Ort, * an dem ein Fehler stehen könnte: das Menü ist in dem Augenblick schon zu. * Vorher verschwand eine abgelehnte Anfrage deshalb spurlos — der Klick tat * sichtbar nichts, und nichts sagte warum. * * **Sie nimmt den Fokus nicht.** `aria-live="polite"` liest sie vor, sobald * der Benutzer eine Pause macht; ihn mitten im Tun zu unterbrechen wäre für * eine Randnotiz zu viel. */ export function hinweisZeigen(text: string, art: 'gut' | 'fehler' = 'gut'): void { let bereich = document.querySelector('.hinweise'); if (!bereich) { bereich = document.createElement('div'); bereich.className = 'hinweise'; bereich.setAttribute('aria-live', 'polite'); document.body.appendChild(bereich); } const zeile = document.createElement('div'); zeile.className = `hinweiszeile ${art}`; zeile.textContent = text; bereich.appendChild(zeile); // Vier Sekunden: lang genug zum Lesen, kurz genug, um nicht im Weg zu // stehen. Das Wegblenden darf nicht springen, deshalb erst die Klasse, // dann das Entfernen nach dem Übergang. window.setTimeout(() => { zeile.classList.add('geht'); window.setTimeout(() => { zeile.remove(); if (bereich && bereich.childElementCount === 0) bereich.remove(); }, 200); }, 4000); }