/* 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();
});