/* WFW-Aushang PWA * - Listet Dateien aus public WebDAV Share * - Öffnet PDFs über PDF.js * - Kioskiges Install/Update Overlay */ const SHARE_TOKEN = "T9e7WESBXxy6rSD"; const SHARE_ROOT = "/"; // im Share const DAV_BASE = `https://home.x-s.at/public.php/dav/files/${SHARE_TOKEN}`; const PDFJS_VIEWER = "https://home.x-s.at/pdfjs/web/viewer.html?file="; // Versioning für Overlay / Update-Erkennung const APP_VERSION = "2025.12.24.2"; const DISMISS_KEY = "wfw_overlay_dismissed_for"; const VERSION_URL = "./version.json"; function $(id){ return document.getElementById(id); } function applyTheme(theme){ document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("wfw_theme", theme); } function initTheme(){ const saved = localStorage.getItem("wfw_theme"); if (saved) return applyTheme(saved); const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; applyTheme(prefersDark ? "dark" : "light"); } function isInstalledPWA(){ const m = window.matchMedia ? window.matchMedia("(display-mode: standalone)") : null; const m2 = window.matchMedia ? window.matchMedia("(display-mode: fullscreen)") : null; const isStandalone = !!(m && m.matches); const isFullscreen = !!(m2 && m2.matches); const isIOSStandalone = window.navigator.standalone === true; return isStandalone || isFullscreen || isIOSStandalone; } function showOverlay({title, text, primaryText, hint=""}){ $("ovTitle").textContent = title; $("ovText").textContent = text; $("ovPrimaryBtn").textContent = primaryText; $("ovHint").textContent = hint; $("installOverlay").hidden = false; } function hideOverlay(){ $("installOverlay").hidden = true; } function markOverlayDismissed(latestVersion){ sessionStorage.setItem(DISMISS_KEY, latestVersion || APP_VERSION); } function isOverlayDismissed(latestVersion){ const v = sessionStorage.getItem(DISMISS_KEY); return v && v === (latestVersion || APP_VERSION); } let deferredPrompt = null; window.addEventListener("beforeinstallprompt", (e) => { e.preventDefault(); deferredPrompt = e; }); // --- Service Worker --- async function registerSW(){ if (!("serviceWorker" in navigator)) return null; try { const reg = await navigator.serviceWorker.register("./sw.js"); // Optional: updaten, falls neue Version try { await reg.update(); } catch {} return reg; } catch { return null; } } // --- Install / Update Overlay --- async function checkInstallOrUpdateOverlay(reg){ let latest = APP_VERSION; try { const r = await fetch(VERSION_URL, { cache: "no-store" }); if (r.ok) { const j = await r.json(); if (j && typeof j.version === "string") latest = j.version; } } catch {} const installed = isInstalledPWA(); const outdated = (latest !== APP_VERSION); // Wenn installiert UND aktuell -> kein Overlay if (installed && !outdated) return; // Wenn veraltet -> "Aktualisieren" erzwingen (kioskig) if (outdated) { showOverlay({ title: "Update verfügbar", text: `Es gibt eine neue Version (${latest}). Bitte aktualisieren.`, primaryText: "Aktualisieren", hint: "Tipp: Nach dem Update wird die App neu geladen." }); $("ovPrimaryBtn").onclick = async () => { try { await reg?.update?.(); } catch {} // Cache-bust / reload location.reload(); }; $("ovContinueBtn").onclick = () => hideOverlay(); return; } // Nicht installiert -> "Installieren" (oder Hinweis wenn Prompt fehlt) const canInstall = !!deferredPrompt && !installed; if (canInstall) { showOverlay({ title: "App installieren", text: "Installiert läuft WFW-Aushang schneller und im Kiosk-Modus.", primaryText: "Installieren", hint: "" }); $("ovPrimaryBtn").onclick = async () => { try { deferredPrompt.prompt(); await deferredPrompt.userChoice; } catch {} deferredPrompt = null; hideOverlay(); }; } else { // Fallback: wenn Chrome (noch) keinen Install-Prompt gibt showOverlay({ title: "App installieren", text: "Wenn der Install-Button fehlt: Chrome Menü (⋮) → „App installieren“.", primaryText: "OK", hint: "" }); $("ovPrimaryBtn").onclick = () => hideOverlay(); } $("ovContinueBtn").onclick = () => hideOverlay(); } // --- WebDAV Listing via PROPFIND --- async function propfind(url, depth=1){ const res = await fetch(url, { method: "PROPFIND", headers: { "Depth": String(depth) } }); if (!res.ok) { const t = await res.text().catch(()=> ""); throw new Error(`PROPFIND ${res.status} ${res.statusText} ${t.slice(0,200)}`); } return await res.text(); } function parseDavResponse(xmlText){ const parser = new DOMParser(); const xml = parser.parseFromString(xmlText, "application/xml"); const responses = [...xml.getElementsByTagNameNS("*","response")]; const items = responses.map(r => { const hrefEl = r.getElementsByTagNameNS("*","href")[0]; const href = hrefEl ? hrefEl.textContent : ""; const propstat = r.getElementsByTagNameNS("*","propstat")[0]; const prop = propstat ? propstat.getElementsByTagNameNS("*","prop")[0] : null; const isCollection = !!(prop && prop.getElementsByTagNameNS("*","collection")[0]); const sizeEl = prop ? prop.getElementsByTagNameNS("*","getcontentlength")[0] : null; const size = sizeEl ? parseInt(sizeEl.textContent || "0", 10) : 0; const modEl = prop ? prop.getElementsByTagNameNS("*","getlastmodified")[0] : null; const lastmod = modEl ? modEl.textContent : ""; return { href, isCollection, size, lastmod }; }); return items; } function humanSize(bytes){ if (!bytes || bytes < 1024) return bytes ? `${bytes} B` : ""; const kb = bytes/1024; if (kb < 1024) return `${kb.toFixed(0)} KB`; const mb = kb/1024; return `${mb.toFixed(1)} MB`; } function safeDecode(s){ try { return decodeURIComponent(s); } catch { return s; } } function extractNameFromHref(href){ const noQuery = href.split("?")[0]; const parts = noQuery.split("/").filter(Boolean); const last = parts[parts.length-1] || ""; return safeDecode(last); } function pdfUrlFromHref(href){ // href ist serverseitig percent-encoded; wir wollen eine saubere absolute URL für public.php/dav // href kommt meist als /public.php/dav/files//... oder voll if (href.startsWith("http")) return href; return `https://home.x-s.at${href}`; } function openPdfInPdfJs(fileUrl){ // PDF.js erwartet file=ENCODED_URL const full = PDFJS_VIEWER + encodeURIComponent(fileUrl) + "#zoom=page-width"; window.location.href = full; } function renderList(files){ const list = $("list"); list.innerHTML = ""; if (!files.length) { $("status").textContent = "Keine PDFs gefunden."; return; } for (const f of files) { const row = document.createElement("div"); row.className = "item"; const icon = document.createElement("div"); icon.className = "pdfIcon"; icon.textContent = "📄"; const name = document.createElement("div"); name.className = "name"; name.textContent = f.name; const meta = document.createElement("div"); meta.className = "meta"; meta.textContent = humanSize(f.size); row.appendChild(icon); row.appendChild(name); row.appendChild(meta); row.onclick = () => openPdfInPdfJs(f.url); list.appendChild(row); } $("status").textContent = `PDFs: ${files.length}`; } async function load(){ $("status").textContent = "Lade Liste…"; const basePath = SHARE_ROOT.replace(/^\/?/, "/").replace(/\/?$/, "/"); const url = DAV_BASE + basePath; // z.B. .../TOKEN/ const xml = await propfind(url, 1); const items = parseDavResponse(xml); // Erstes Element ist meistens das Verzeichnis selbst -> rausfiltern const files = items .filter(it => !it.isCollection) .map(it => { const name = extractNameFromHref(it.href); return { name, size: it.size || 0, url: pdfUrlFromHref(it.href) }; }) .filter(f => f.name.toLowerCase().endsWith(".pdf")); // Sortierung: nach Dateiname absteigend (oft Datum im Namen) files.sort((a,b) => b.name.localeCompare(a.name, "de")); renderList(files); } function closeAushang(){ // kiosk-typisch: zurück oder tab schließen (falls erlaubt) try { window.close(); } catch {} history.back(); } // Boot document.addEventListener("DOMContentLoaded", async () => { initTheme(); $("themeBtn").addEventListener("click", () => { const cur = document.documentElement.getAttribute("data-theme") || "light"; applyTheme(cur === "dark" ? "light" : "dark"); }); $("refreshBtn").addEventListener("click", () => load()); $("closeBtn").addEventListener("click", closeAushang); const reg = await registerSW(); await checkInstallOrUpdateOverlay(reg); try { await load(); } catch (e) { $("status").textContent = "Fehler beim Laden der Liste."; // In kiosk mode: Fehlertext kurz, Details nur im Overlay-Hint showOverlay({ title: "Fehler", text: "Die Liste konnte nicht geladen werden.", primaryText: "OK", hint: String(e).slice(0, 140) }); $("ovPrimaryBtn").onclick = () => hideOverlay(); $("ovContinueBtn").onclick = () => hideOverlay(); } });