403 lines
12 KiB
JavaScript
403 lines
12 KiB
JavaScript
/* 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 = `<?xml version="1.0" encoding="utf-8"?>
|
|
<d:propfind xmlns:d="DAV:">
|
|
<d:prop>
|
|
<d:displayname/>
|
|
<d:getcontenttype/>
|
|
<d:getcontentlength/>
|
|
<d:resourcetype/>
|
|
<d:getlastmodified/>
|
|
</d:prop>
|
|
</d:propfind>`;
|
|
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 = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
|
<path d="M7 3h7l3 3v15a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" stroke="currentColor" stroke-width="2" opacity=".9"/>
|
|
<path d="M14 3v4h4" stroke="currentColor" stroke-width="2" opacity=".9"/>
|
|
<path d="M7.5 14.5c2-2.5 3.5-5 4.2-7.5 1.1 3.8 2.8 7 4.8 9.5" stroke="currentColor" stroke-width="2" opacity=".9"/>
|
|
</svg>`;
|
|
|
|
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();
|
|
});
|