/* WFW-Aushang PWA * - Listet Dateien aus public WebDAV Share * - Öffnet PDFs über PDF.js * - Install/Update Overlay mit beforeinstallprompt.prompt() */ const APP_VERSION = "2025.12.24.19"; const TOKEN = "T9e7WESBXxy6rSD"; const BASE = "https://home.x-s.at"; const SHARE_DAV = `${BASE}/public.php/dav/files/${TOKEN}`; const PDFJS = `${BASE}/pdfjs/web/viewer.html?file=`; // relative to /zuss/ const VERSION_URL = "./version.json"; const LIST_START_DIR = "/"; // share root let deferredInstallPrompt = null; let latestKnown = APP_VERSION; const $ = (id) => document.getElementById(id); function showError(msg) { const el = $("err"); el.style.display = "block"; el.textContent = msg; } function clearError() { const el = $("err"); el.style.display = "none"; el.textContent = ""; } function setStatus(msg) { $("status").textContent = msg; } function showOverlay(title, text, primaryText, showPrimary = true) { $("ovTitle").textContent = title; $("ovText").textContent = text; $("ovPrimaryBtn").textContent = primaryText; $("ovPrimaryBtn").style.display = showPrimary ? "flex" : "none"; $("ovHint").style.display = "none"; $("installOverlay").style.display = "block"; } function hideOverlay() { $("installOverlay").style.display = "none"; } function showManualInstallHint() { $("ovHint").style.display = "block"; $("ovHint").textContent = "Hinweis: Falls kein Install-Dialog erscheint: Chrome Menü (⋮) → „App installieren“ / „Zum Startbildschirm hinzufügen“."; } function compareVersions(a, b) { // "2025.12.24.3" style; compare numeric segments const pa = String(a).split(".").map(n => parseInt(n, 10) || 0); const pb = String(b).split(".").map(n => parseInt(n, 10) || 0); const len = Math.max(pa.length, pb.length); for (let i = 0; i < len; i++) { const da = pa[i] || 0, db = pb[i] || 0; if (da > db) return 1; if (da < db) return -1; } return 0; } async function fetchLatestVersion() { try { const r = await fetch(VERSION_URL, { cache: "no-store" }); if (!r.ok) throw new Error(`version.json HTTP ${r.status}`); const j = await r.json(); if (j && j.version) { latestKnown = j.version; sessionStorage.setItem("wfw_latest", latestKnown); } } catch (e) { // not fatal console.warn("Could not fetch version.json:", e); } } function getInstalledVersion() { return localStorage.getItem("wfw_installed_version") || null; } function markInstalledVersion(v) { localStorage.setItem("wfw_installed_version", v); } function markOverlayDismissed(v) { localStorage.setItem("wfw_dismissed_version", v); } function getDismissedVersion() { return localStorage.getItem("wfw_dismissed_version") || null; } async function checkInstallOrUpdateOverlay() { await fetchLatestVersion(); const installed = getInstalledVersion(); const dismissed = getDismissedVersion(); const isStandalone = window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true; // If installed version < latest -> show update overlay if (installed && compareVersions(installed, latestKnown) < 0) { showOverlay( "Update verfügbar", `Neue Version verfügbar (${latestKnown}). Bitte updaten für die neuesten Fixes.`, "Update jetzt" ); $("ovPrimaryBtn").onclick = async () => { try { // ask SW to update + reload const reg = await navigator.serviceWorker.getRegistration(); if (reg) await reg.update(); } catch {} markInstalledVersion(latestKnown); location.reload(); }; $("ovContinueBtn").onclick = () => { markOverlayDismissed(latestKnown); hideOverlay(); }; return; } // If not installed (or standalone not detected), show install overlay until latest dismissed/installed if (!installed && dismissed !== latestKnown && !isStandalone) { showOverlay( "WFW-Aushang", "Installiere mich!", "Installieren" ); $("ovPrimaryBtn").onclick = async (e) => { e.preventDefault(); e.stopPropagation(); await doInstall(); }; $("ovContinueBtn").onclick = (e) => { e.preventDefault(); e.stopPropagation(); markOverlayDismissed(latestKnown); hideOverlay(); }; } } async function doInstall() { if (!deferredInstallPrompt) { showManualInstallHint(); return; } try { deferredInstallPrompt.prompt(); // <-- IMPORTANT const res = await deferredInstallPrompt.userChoice; deferredInstallPrompt = null; if (res && res.outcome === "accepted") { // We'll also set installed version on appinstalled event $("ovTitle").textContent = "Installiert ✓"; $("ovText").textContent = "Danke! Du kannst die App jetzt wie eine normale App starten."; markInstalledVersion(latestKnown); setTimeout(() => hideOverlay(), 800); } else { showManualInstallHint(); } } catch (e) { console.warn("Install prompt failed:", e); showManualInstallHint(); } } /** WebDAV listing **/ async function propfind(dir) { const url = `${SHARE_DAV}${dir.endsWith("/") ? dir : dir + "/"}`; const body = ` `; const r = await fetch(url, { method: "PROPFIND", headers: { "Depth": "1", "Content-Type": "application/xml" }, body }); if (!r.ok) { const t = await r.text().catch(()=> ""); throw new Error(`PROPFIND ${r.status} ${r.statusText}\n${t.slice(0, 400)}`); } return await r.text(); } function parseDavMultistatus(xmlText, dir) { const p = new DOMParser(); const xml = p.parseFromString(xmlText, "application/xml"); const responses = Array.from(xml.getElementsByTagNameNS("DAV:", "response")); const items = []; for (const resp of responses) { const hrefEl = resp.getElementsByTagNameNS("DAV:", "href")[0]; if (!hrefEl) continue; const href = decodeURIComponent(hrefEl.textContent || ""); // Skip the directory itself (first response) const shareRoot = `/public.php/dav/files/${TOKEN}`; if (href.endsWith(shareRoot + (dir === "/" ? "/" : dir))) continue; const displayEl = resp.getElementsByTagNameNS("DAV:", "displayname")[0]; const name = displayEl ? (displayEl.textContent || "").trim() : ""; const rtEl = resp.getElementsByTagNameNS("DAV:", "resourcetype")[0]; const isCollection = rtEl && rtEl.getElementsByTagNameNS("DAV:", "collection").length > 0; const ctEl = resp.getElementsByTagNameNS("DAV:", "getcontenttype")[0]; const contentType = ctEl ? (ctEl.textContent || "").trim() : ""; const lmEl = resp.getElementsByTagNameNS("DAV:", "getlastmodified")[0]; const lastMod = lmEl ? (lmEl.textContent || "").trim() : ""; items.push({ href, name: name || href.split("/").filter(Boolean).slice(-1)[0] || "", isDir: !!isCollection, contentType, lastMod }); } // directories first, then by name items.sort((a,b)=> (a.isDir === b.isDir ? a.name.localeCompare(b.name) : (a.isDir ? -1 : 1))); return items; } function toPdfJsUrl(publicPath) { // publicPath must be the share file path relative to share root, starting with "/" const fileUrl = `${SHARE_DAV}${publicPath}`; // direct public dav file URL return `${PDFJS}${encodeURIComponent(fileUrl)}#zoom=page-width`; } function renderList(items, currentDir) { const list = $("list"); list.innerHTML = ""; // no folder browsing (kiosk): show only PDFs in root+subdirs? you asked "only directory content" of share root. // We'll show only currentDir content; no folder nav UI. const pdfs = items.filter(it => !it.isDir && (it.name.toLowerCase().endsWith(".pdf") || it.contentType.includes("pdf"))); if (pdfs.length === 0) { setStatus("Keine PDFs gefunden."); const cnt = $("titleCount"); if (cnt) cnt.textContent = "(0)"; return; } setStatus(""); const cnt = $("titleCount"); if (cnt) cnt.textContent = `(${pdfs.length})`; for (const it of pdfs) { const a = document.createElement("a"); a.className = "row"; // Build relative path within share for viewer URL const marker = `/public.php/dav/files/${TOKEN}`; const idx = it.href.indexOf(marker); let rel = "/"; if (idx >= 0) rel = it.href.slice(idx + marker.length); if (!rel.startsWith("/")) rel = "/" + rel; const targetUrl = toPdfJsUrl(rel); a.href = targetUrl; a.target = "_blank"; a.rel = "noopener noreferrer"; const ico = document.createElement("div"); ico.className = "ico"; ico.innerHTML = ` `; const mid = document.createElement("div"); const nm = document.createElement("div"); nm.className = "name"; nm.textContent = it.name; const sub = document.createElement("div"); sub.className = "sub"; sub.textContent = it.lastMod ? it.lastMod : ""; mid.appendChild(nm); mid.appendChild(sub); const badge = document.createElement("div"); badge.className = "badge"; badge.textContent = "PDF"; a.appendChild(ico); a.appendChild(mid); a.appendChild(document.createElement("div")).className = "spacer"; a.appendChild(badge); list.appendChild(a); } } async function loadList() { clearError(); setStatus("Lade Liste…"); try { const xml = await propfind(LIST_START_DIR); const items = parseDavMultistatus(xml, LIST_START_DIR); renderList(items, LIST_START_DIR); } catch (e) { console.error(e); setStatus("Fehler"); const cnt = $("titleCount"); if (cnt) cnt.textContent = ""; showError(String(e && e.message ? e.message : e)); } } async function registerSW() { if (!("serviceWorker" in navigator)) return; try { const reg = await navigator.serviceWorker.register("./sw.js", { scope: "./" }); // Trigger update check reg.update().catch(()=>{}); } catch (e) { console.warn("SW register failed:", e); } } function closeAushang(){ try { window.close(); } catch {} // Fallbacks: first try history back, then blank setTimeout(() => { if (document.visibilityState === "hidden") return; // tab closed history.back(); setTimeout(() => { if (document.visibilityState === "hidden") return; location.replace("about:blank"); }, 200); }, 120); } function applyTheme(t){ document.documentElement.setAttribute("data-theme", t); localStorage.setItem("wfw_theme", t); $("themeBtn").textContent = t === "dark" ? "🌙 Dunkel" : "☀️ Hell"; } function initTheme(){ const saved = localStorage.getItem("wfw_theme"); if (saved){ applyTheme(saved); } else { // Default: System const prefersLight = window.matchMedia("(prefers-color-scheme: light)").matches; applyTheme(prefersLight ? "light" : "dark"); } } window.addEventListener("appinstalled", () => { // user installed the app markInstalledVersion(latestKnown); markOverlayDismissed(latestKnown); hideOverlay(); }); window.addEventListener("beforeinstallprompt", (e) => { // We want our own overlay, so we prevent default banner e.preventDefault(); deferredInstallPrompt = e; // Show overlay if not yet installed checkInstallOrUpdateOverlay(); }); // Extra safety: capture clicks even if something gets weird on mobile document.addEventListener("click", (e) => { const t = e.target; if (!t) return; if (t.id === "ovContinueBtn") { e.preventDefault(); e.stopPropagation(); markOverlayDismissed(latestKnown); hideOverlay(); } if (t.id === "ovPrimaryBtn") { // If handler was overwritten, still attempt install/update // (doInstall will show manual hint if prompt not available) } }, { capture: true }); document.addEventListener("DOMContentLoaded", async () => { $("refreshBtn").addEventListener("click", (e) => { e.preventDefault(); loadList(); }); $("closeBtn").addEventListener("click", (e) => { e.preventDefault(); closeAushang(); }); initTheme(); $("themeBtn").addEventListener("click", () => { const current = document.documentElement.getAttribute("data-theme") || "dark"; applyTheme(current === "dark" ? "light" : "dark"); }); await registerSW(); await checkInstallOrUpdateOverlay(); await loadList(); });